diff --git a/CHANGELOG.md b/CHANGELOG.md index 4900488e..d70a2af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased ### Fixed +- List hosts (Issue [#331](https://github.com/fabric-testbed/fabrictestbed-extensions/issues/331)) - AL2S Support (Issue [#325](https://github.com/fabric-testbed/fabrictestbed-extensions/issues/325)) - Deny infeasible slices (Issue [#326](https://github.com/fabric-testbed/fabrictestbed-extensions/issues/326)) - Add display of switch port name to network service table listing (Issue [#152](https://github.com/fabric-testbed/fabrictestbed-extensions/issues/152)) diff --git a/fabrictestbed_extensions/fablib/constants.py b/fabrictestbed_extensions/fablib/constants.py index 58e5b0d8..f53e98ef 100644 --- a/fabrictestbed_extensions/fablib/constants.py +++ b/fabrictestbed_extensions/fablib/constants.py @@ -159,3 +159,27 @@ class Constants: SSH_KEYS = "sshkeys" EXPIRES_ON = "expires_on" LEASE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S %z" + + NON_PRETTY_NAME = "non_pretty_name" + PRETTY_NAME = "pretty_name" + HEADER_NAME = "header_name" + AVAILABLE = "Available" + CAPACITY = "Capacity" + ALLOCATED = "Allocated" + VALUE = "value" + + NIC_SHARED_CONNECTX_6 = "SharedNIC-ConnectX-6" + SMART_NIC_CONNECTX_6 = "SmartNIC-ConnectX-6" + SMART_NIC_CONNECTX_5 = "SmartNIC-ConnectX-5" + NVME_P4510 = "NVME-P4510" + GPU_TESLA_T4 = "GPU-Tesla T4" + GPU_RTX6000 = "GPU-RTX6000" + GPU_A30 = "GPU-A30" + GPU_A40 = "GPU-A40" + FPGA_XILINX_U280 = "FPGA-Xilinx-U280" + CORES = "Cores" + RAM = "Ram" + DISK = "Disk" + CPUS = "CPUs" + HOSTS = "Hosts" + P4_SWITCH = "P4-Switch" diff --git a/fabrictestbed_extensions/fablib/fablib.py b/fabrictestbed_extensions/fablib/fablib.py index 74fc9a9b..e731cf4e 100644 --- a/fabrictestbed_extensions/fablib/fablib.py +++ b/fabrictestbed_extensions/fablib/fablib.py @@ -72,6 +72,8 @@ import traceback import warnings +from fabrictestbed_extensions.fablib.site import Host, Site + warnings.filterwarnings("always", category=DeprecationWarning) from concurrent.futures import ThreadPoolExecutor @@ -143,6 +145,16 @@ def list_sites(latlon: bool = True) -> object: """ return fablib.get_default_fablib_manager().list_sites(latlon=latlon) + @staticmethod + def list_hosts() -> object: + """ + Get a string used to print a tabular list of sites with state + + :return: tabulated string of site state + :rtype: str + """ + return fablib.get_default_fablib_manager().list_hosts() + @staticmethod def list_links() -> object: """ @@ -1137,6 +1149,78 @@ def list_sites( latlon=latlon, ) + def list_hosts( + self, + output: str = None, + fields: str = None, + quiet: bool = False, + filter_function=None, + update: bool = True, + pretty_names: bool = True, + force_refresh: bool = False, + start: datetime = None, + end: datetime = None, + avoid: List[str] = None, + includes: List[str] = None, + ) -> object: + """ + Lists all the hosts and their attributes. + + There are several output options: "text", "pandas", and "json" that determine the format of the + output that is returned and (optionally) displayed/printed. + + output: 'text': string formatted with tabular + 'pandas': pandas dataframe + 'json': string in json format + + fields: json output will include all available fields/columns. + + Example: fields=['Name','ConnectX-5 Available', 'NVMe Total'] + + filter_function: A lambda function to filter data by field values. + + Example: filter_function=lambda s: s['ConnectX-5 Available'] > 3 and s['NVMe Available'] <= 10 + + :param output: output format + :type output: str + :param fields: list of fields (table columns) to show + :type fields: List[str] + :param quiet: True to specify printing/display + :type quiet: bool + :param filter_function: lambda function + :type filter_function: lambda + :return: table in format specified by output parameter + :param update: + :type update: bool + :param pretty_names: + :type pretty_names: bool + :param force_refresh: + :type force_refresh: bool + :param start: start time in UTC format: %Y-%m-%d %H:%M:%S %z + :type: datetime + :param end: end time in UTC format: %Y-%m-%d %H:%M:%S %z + :type: datetime + :param avoid: list of sites to avoid + :type: list of string + :param includes: list of sites to include + :type: list of string + + """ + return self.get_resources( + update=update, + force_refresh=force_refresh, + start=start, + end=end, + avoid=avoid, + includes=includes, + ).list_hosts( + output=output, + fields=fields, + quiet=quiet, + filter_function=filter_function, + pretty_names=pretty_names, + ) + def list_links( self, output: str = None, @@ -2284,86 +2368,62 @@ def create_show_table(data, fields=None, pretty_names_dict={}): return table @staticmethod - def __can_allocate_node_in_worker( - worker: FimNode, node: Node, allocated: dict, site: FimNode + def __can_allocate_node_in_host( + host: Host, node: Node, allocated: dict, site: Site ) -> Tuple[bool, str]: """ - Check if a node can be provisioned on a worker node on a site w.r.t available resources on that site + Check if a node can be provisioned on a host node on a site w.r.t available resources on that site :return: Tuple indicating status for validation and error message in case of failure :rtype: Tuple[bool, str] """ - if worker is None or site is None: + if host is None or site is None: return ( True, - f"Ignoring validation: Worker: {worker}, Site: {site} not available.", + f"Ignoring validation: Host: {host}, Site: {site} not available.", ) - msg = f"Node can be allocated on the host: {worker.name}." + msg = f"Node can be allocated on the host: {host.get_name()}." - worker_maint_info = site.maintenance_info.get(worker.name) - if worker_maint_info and str(worker_maint_info.state) != "Active": - msg = f"Node cannot be allocated on {worker.name}, {worker.name} is in {worker_maint_info.state}!" + if host.get_state() != "Active": + msg = f"Node cannot be allocated on {host.get_name()}, {host.get_name()} is in {host.get_state()}!" return False, msg allocated_core = allocated.setdefault("core", 0) allocated_ram = allocated.setdefault("ram", 0) allocated_disk = allocated.setdefault("disk", 0) - available_cores = ( - worker.capacities.core - - ( - worker.capacity_allocations.core - if worker.capacity_allocations is not None - else 0 - ) - - allocated_core - ) - available_ram = ( - worker.capacities.ram - - ( - worker.capacity_allocations.ram - if worker.capacity_allocations is not None - else 0 - ) - - allocated_ram - ) - available_disk = ( - worker.capacities.disk - - ( - worker.capacity_allocations.disk - if worker.capacity_allocations is not None - else 0 - ) - - allocated_disk - ) + available_cores = host.get_core_available() + available_ram = host.get_ram_available() + available_disk = host.get_disk_available() if ( node.get_requested_cores() > available_cores or node.get_requested_disk() > available_disk or node.get_requested_ram() > available_ram ): - msg = f"Insufficient Resources: Host: {worker.name} does not meet core/ram/disk requirements." + msg = f"Insufficient Resources: Host: {host.get_name()} does not meet core/ram/disk requirements." return False, msg # Check if there are enough components available for c in node.get_components(): comp_model_type = f"{c.get_type()}-{c.get_fim_model()}" - if comp_model_type not in worker.components: - msg = f"Invalid Request: Host: {worker.name} does not have the requested component: {comp_model_type}." + substrate_component = host.get_component(comp_model_type=comp_model_type) + if not substrate_component: + msg = f"Invalid Request: Host: {host.get_name()} does not have the requested component: {comp_model_type}." return False, msg allocated_comp_count = allocated.setdefault(comp_model_type, 0) available_comps = ( - worker.components[comp_model_type].capacities.unit + substrate_component.capacities.unit - ( - worker.components[comp_model_type].capacity_allocations.unit - if worker.components[comp_model_type].capacity_allocations + substrate_component.capacity_allocations.unit + if substrate_component.capacity_allocations else 0 ) - allocated_comp_count ) if available_comps <= 0: - msg = f"Insufficient Resources: Host: {worker.name} has reached the limit for component: {comp_model_type}." + msg = f"Insufficient Resources: Host: {host.get_name()} has reached the limit for component: {comp_model_type}." return False, msg allocated[comp_model_type] += 1 @@ -2385,7 +2445,7 @@ def validate_node(self, node: Node, allocated: dict = None) -> Tuple[bool, str]: error = None if allocated is None: allocated = {} - site = self.get_resources().get_topology_site(site_name=node.get_site()) + site = self.get_resources().get_site(site_name=node.get_site()) if not site: logging.warning( @@ -2396,43 +2456,43 @@ def validate_node(self, node: Node, allocated: dict = None) -> Tuple[bool, str]: f"Ignoring validation: Site: {node.get_site()} not available in resources.", ) - site_maint_info = site.maintenance_info.get(site.name) - if site_maint_info and str(site_maint_info.state) != "Active": - msg = f"Node cannot be allocated on {node.get_site()}, {node.get_site()} is in {site_maint_info.state}." + site_state = site.get_state() + if site_state != "Active": + msg = f"Node cannot be allocated on {node.get_site()}, {node.get_site()} is in {site_state}." logging.error(msg) return False, msg - workers = self.get_resources().get_nodes(site=site) - if not workers: + hosts = site.get_hosts() + if not hosts: msg = f"Node cannot be validated, host information not available for {site}." logging.error(msg) return False, msg if node.get_host(): - if node.get_host() not in workers: + if node.get_host() not in hosts: msg = f"Invalid Request: Requested Host {node.get_host()} does not exist on site: {node.get_site()}." logging.error(msg) return False, msg - worker = workers.get(node.get_host()) + host = hosts.get(node.get_host()) allocated_comps = allocated.setdefault(node.get_host(), {}) - status, error = self.__can_allocate_node_in_worker( - worker=worker, node=node, allocated=allocated_comps, site=site + status, error = self.__can_allocate_node_in_host( + host=host, node=node, allocated=allocated_comps, site=site ) if not status: logging.error(error) return status, error - for worker in workers.values(): - allocated_comps = allocated.setdefault(worker.name, {}) - status, error = self.__can_allocate_node_in_worker( - worker=worker, node=node, allocated=allocated_comps, site=site + for host in hosts.values(): + allocated_comps = allocated.setdefault(host.get_name(), {}) + status, error = self.__can_allocate_node_in_host( + host=host, node=node, allocated=allocated_comps, site=site ) if status: return status, error - msg = f"Invalid Request: Requested Node cannot be accommodated by any of the hosts on site: {site.name}." + msg = f"Invalid Request: Requested Node cannot be accommodated by any of the hosts on site: {site.get_name()}." if error: msg += f" Details: {error}" logging.error(msg) diff --git a/fabrictestbed_extensions/fablib/resources.py b/fabrictestbed_extensions/fablib/resources.py index f1f7a39c..1c14aa30 100644 --- a/fabrictestbed_extensions/fablib/resources.py +++ b/fabrictestbed_extensions/fablib/resources.py @@ -33,127 +33,18 @@ import json import logging -import traceback from datetime import datetime -from typing import Dict, List, Tuple +from typing import List, Tuple -from fabrictestbed.slice_editor import AdvertisedTopology, Capacities +from fabrictestbed.slice_editor import AdvertisedTopology from fabrictestbed.slice_manager import Status -from fim.slivers import network_node from fim.user import interface, link, node -from fim.view_only_dict import ViewOnlyDict from tabulate import tabulate +from fabrictestbed_extensions.fablib.site import ResourceConstants, Site -class Resources: - NON_PRETTY_NAME = "non_pretty_name" - PRETTY_NAME = "pretty_name" - HEADER_NAME = "header_name" - AVAILABLE = "Available" - CAPACITY = "Capacity" - ALLOCATED = "Allocated" - VALUE = "value" - - NIC_SHARED_CONNECTX_6 = "SharedNIC-ConnectX-6" - SMART_NIC_CONNECTX_6 = "SmartNIC-ConnectX-6" - SMART_NIC_CONNECTX_5 = "SmartNIC-ConnectX-5" - NVME_P4510 = "NVME-P4510" - GPU_TESLA_T4 = "GPU-Tesla T4" - GPU_RTX6000 = "GPU-RTX6000" - GPU_A30 = "GPU-A30" - GPU_A40 = "GPU-A40" - FPGA_XILINX_U280 = "FPGA-Xilinx-U280" - CORES = "Cores" - RAM = "Ram" - DISK = "Disk" - CPUS = "CPUs" - HOSTS = "Hosts" - - site_attribute_name_mappings = { - CORES.lower(): { - NON_PRETTY_NAME: CORES.lower(), - PRETTY_NAME: CORES, - HEADER_NAME: CORES, - }, - RAM.lower(): { - NON_PRETTY_NAME: RAM.lower(), - PRETTY_NAME: RAM, - HEADER_NAME: f"{RAM} ({Capacities.UNITS[RAM.lower()]})", - }, - DISK: { - NON_PRETTY_NAME: DISK.lower(), - PRETTY_NAME: DISK, - HEADER_NAME: f"{DISK} ({Capacities.UNITS[DISK.lower()]})", - }, - NIC_SHARED_CONNECTX_6: { - NON_PRETTY_NAME: "nic_basic", - PRETTY_NAME: "Basic NIC", - HEADER_NAME: "Basic (100 Gbps NIC)", - }, - SMART_NIC_CONNECTX_6: { - NON_PRETTY_NAME: "nic_connectx_6", - PRETTY_NAME: "ConnectX-6", - HEADER_NAME: "ConnectX-6 (100 Gbps x2 NIC)", - }, - SMART_NIC_CONNECTX_5: { - NON_PRETTY_NAME: "nic_connectx_5", - PRETTY_NAME: "ConnectX-5", - HEADER_NAME: "ConnectX-5 (25 Gbps x2 NIC)", - }, - NVME_P4510: { - NON_PRETTY_NAME: "nvme", - PRETTY_NAME: "NVMe", - HEADER_NAME: "P4510 (NVMe 1TB)", - }, - GPU_TESLA_T4: { - NON_PRETTY_NAME: "tesla_t4", - PRETTY_NAME: "Tesla T4", - HEADER_NAME: "Tesla T4 (GPU)", - }, - GPU_RTX6000: { - NON_PRETTY_NAME: "rtx6000", - PRETTY_NAME: "RTX6000", - HEADER_NAME: "RTX6000 (GPU)", - }, - GPU_A30: { - NON_PRETTY_NAME: "a30", - PRETTY_NAME: "A30", - HEADER_NAME: "A30 (GPU)", - }, - GPU_A40: { - NON_PRETTY_NAME: "a40", - PRETTY_NAME: "A40", - HEADER_NAME: "A40 (GPU)", - }, - FPGA_XILINX_U280: { - NON_PRETTY_NAME: "fpga_u280", - PRETTY_NAME: "U280", - HEADER_NAME: "FPGA-Xilinx-U280", - }, - } - site_pretty_names = { - "name": "Name", - "state": "State", - "address": "Address", - "location": "Location", - "ptp_capable": "PTP Capable", - HOSTS.lower(): HOSTS, - CPUS.lower(): CPUS, - } - for attribute, names in site_attribute_name_mappings.items(): - non_pretty_name = names.get(NON_PRETTY_NAME) - pretty_name = names.get(PRETTY_NAME) - site_pretty_names[non_pretty_name] = pretty_name - site_pretty_names[f"{non_pretty_name}_{AVAILABLE.lower()}"] = ( - f"{pretty_name} {AVAILABLE}" - ) - site_pretty_names[f"{non_pretty_name}_{CAPACITY.lower()}"] = ( - f"{pretty_name} {CAPACITY}" - ) - site_pretty_names[f"{non_pretty_name}_{ALLOCATED.lower()}"] = ( - f"{pretty_name} {ALLOCATED}" - ) +class Resources: def __init__( self, fablib_manager, @@ -190,6 +81,8 @@ def __init__( self.topology = None + self.sites = {} + self.update( force_refresh=force_refresh, start=start, @@ -208,25 +101,9 @@ def __str__(self) -> str: :rtype: String """ table = [] - headers = [ - "Name", - "PTP Capable", - self.CPUS, - ] - for site_name, site in self.topology.sites.items(): - site_info = self.get_site_info(site) - row = [ - site.name, - self.get_ptp_capable(site), - self.get_cpu_capacity(site), - ] - for attribute, names in self.site_attribute_name_mappings.items(): - allocated = site_info.get(attribute, {}).get(self.ALLOCATED.lower(), 0) - capacity = site_info.get(attribute, {}).get(self.CAPACITY.lower(), 0) - available = capacity - allocated - row.append(f"{available}/{capacity}") - headers.append(names.get(self.HEADER_NAME)) - + headers = [] + for site_name, site in self.sites.items(): + headers, row = site.to_row() table.append(row) return tabulate( @@ -250,29 +127,25 @@ def show_site( :param site_name: site name :type site_name: String + :param output: Output type + :type output: str + :param fields: List of fields to include + :type fields: List + :param quiet: flag indicating verbose or quiet display + :type quiet: bool + :param pretty_names: flag indicating if pretty names for the fields to be used or not + :type pretty_names: bool + :param latlon: Flag indicating if lat lon to be included or not + :type latlon: bool + :return: Tabulated string of available resources :rtype: String """ - site = self.topology.sites[site_name] - - data = self.site_to_dict(site, latlon=latlon) - - if pretty_names: - pretty_names_dict = self.site_pretty_names - else: - pretty_names_dict = {} - - site_table = self.get_fablib_manager().show_table( - data, - fields=fields, - title="Site", - output=output, - quiet=quiet, - pretty_names_dict=pretty_names_dict, + site = self.sites.get(site_name) + return site.show( + output=output, fields=fields, quiet=quiet, pretty_names=pretty_names ) - return site_table - def get_site_names(self) -> List[str]: """ Gets a list of all currently available site names @@ -280,130 +153,59 @@ def get_site_names(self) -> List[str]: :return: list of site names :rtype: List[String] """ - site_name_list = [] - for site_name in self.topology.sites.keys(): - site_name_list.append(str(site_name)) + return list(self.sites.keys()) - return site_name_list - - def get_topology_site(self, site_name: str) -> node.Node: + def get_site(self, site_name: str) -> Site: """ - Not recommended for most users. + Get a specific site by name. + + :param site_name: The name of the site to retrieve. + :type site_name: str + + :return: The specified site. + :rtype: Site """ try: - return self.topology.sites[site_name] + return self.sites.get(site_name) except Exception as e: logging.warning(f"Failed to get site {site_name}") - def get_state(self, site: str or node.Node or network_node.NodeSliver) -> str: + def __get_topology_site(self, site_name: str) -> node.Node: """ - Gets the maintenance state of the node + Get a specific site from the topology. - :param site: site Node or NodeSliver object or name - :type site: String or Node or NodeSliver - :return: str(MaintenanceState) - """ - site_name = "" - try: - if isinstance(site, network_node.NodeSliver): - return str(site.maintenance_info.get(site.get_name()).state) - if isinstance(site, node.Node): - return str(site.maintenance_info.get(site.name).state) - return str(self.get_topology_site(site).maintenance_info.get(site).state) - except Exception as e: - # logging.warning(f"Failed to get site state {site_name}") - return "" + :param site_name: The name of the site to retrieve from the topology. + :type site_name: str - def get_nodes(self, site: str or network_node.NodeSliver) -> ViewOnlyDict: - """ - Get worker nodes on a site - :param site: site name - :type site: String + :return: The node representing the specified site from the topology. + :rtype: node.Node """ try: - from fim.graph.abc_property_graph import ABCPropertyGraph - - if isinstance(site, str): - site = self.get_topology_site(site) - - node_id_list = site.topo.graph_model.get_first_neighbor( - node_id=site.node_id, - rel=ABCPropertyGraph.REL_HAS, - node_label=ABCPropertyGraph.CLASS_NetworkNode, - ) - ret = dict() - for nid in node_id_list: - _, node_props = site.topo.graph_model.get_node_properties(node_id=nid) - n = node.Node( - name=node_props[ABCPropertyGraph.PROP_NAME], - node_id=nid, - topo=site.topo, - ) - # exclude Facility nodes - from fim.user import NodeType - - if n.type != NodeType.Facility: - ret[n.name] = n - return ViewOnlyDict(ret) + return self.topology.sites.get(site_name) except Exception as e: - logging.error(f"Error occurred - {e}") - logging.error(traceback.format_exc()) + logging.warning(f"Failed to get site {site_name}") - def get_site_info( - self, site: str or node.Node or network_node.NodeSliver - ) -> Dict[str, Dict[str, int]]: + def get_state(self, site: str or node.Node) -> str: """ - Gets the total site capacity of all components for a site + Gets the maintenance state of the node - :param site: site object or sliver or site name + :param site: site Node or NodeSliver object or name :type site: String or Node or NodeSliver - :return: total component capacity for all components - :rtype: Dict[str, int] + :return: str(MaintenanceState) """ - site_info = {} - try: - nodes = self.get_nodes(site=site) - site_info[self.CORES.lower()] = { - self.CAPACITY.lower(): site.capacities.core, - self.ALLOCATED.lower(): ( - site.capacity_allocations.core if site.capacity_allocations else 0 - ), - } - site_info[self.RAM.lower()] = { - self.CAPACITY.lower(): site.capacities.ram, - self.ALLOCATED.lower(): ( - site.capacity_allocations.ram if site.capacity_allocations else 0 - ), - } - site_info[self.DISK.lower()] = { - self.CAPACITY.lower(): site.capacities.disk, - self.ALLOCATED.lower(): ( - site.capacity_allocations.disk if site.capacity_allocations else 0 - ), - } - - if nodes: - for w in nodes.values(): - if w.components: - for component_model_name, c in w.components.items(): - comp_cap = site_info.setdefault(component_model_name, {}) - comp_cap.setdefault(self.CAPACITY.lower(), 0) - comp_cap.setdefault(self.ALLOCATED.lower(), 0) - comp_cap[self.CAPACITY.lower()] += c.capacities.unit - if c.capacity_allocations: - comp_cap[ - self.ALLOCATED.lower() - ] += c.capacity_allocations.unit - - return site_info + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_name() except Exception as e: - # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") - return site_info + # logging.warning(f"Failed to get site state {site_name}") + return "" def get_component_capacity( self, - site: str or node.Node or network_node.NodeSliver, + site: str or node.Node, component_model_name: str, ) -> int: """ @@ -418,21 +220,21 @@ def get_component_capacity( """ component_capacity = 0 try: - nodes = self.get_nodes(site=site) - if nodes: - for w in nodes.values(): - if component_model_name in w.components: - component_capacity += w.components[ - component_model_name - ].capacities.unit - return component_capacity + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_component_capacity( + component_model_name=component_model_name + ) + except Exception as e: # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") return component_capacity def get_component_allocated( self, - site: str or node.Node or network_node.NodeSliver, + site: str or node.Node, component_model_name: str, ) -> int: """ @@ -448,24 +250,20 @@ def get_component_allocated( """ component_allocated = 0 try: - nodes = self.get_nodes(site=site) - if nodes: - for w in nodes.values(): - if ( - component_model_name in w.components - and w.components[component_model_name].capacity_allocations - ): - component_allocated += w.components[ - component_model_name - ].capacity_allocations.unit - return component_allocated + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_component_allocated( + component_model_name=component_model_name + ) except Exception as e: # logging.error(f"Failed to get {component_model_name} allocated {site}: {e}") return component_allocated def get_component_available( self, - site: str or node.Node or network_node.NodeSliver, + site: str or node.Node, component_model_name: str, ) -> int: """ @@ -480,16 +278,18 @@ def get_component_available( :rtype: int """ try: - return self.get_component_capacity( - site, component_model_name - ) - self.get_component_allocated(site, component_model_name) + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_component_available( + component_model_name=component_model_name + ) except Exception as e: # logging.debug(f"Failed to get {component_model_name} available {site}") return self.get_component_capacity(site, component_model_name) - def get_location_lat_long( - self, site: str or node.Node or network_node.NodeSliver - ) -> Tuple[float, float]: + def get_location_lat_long(self, site: str or node.Node) -> Tuple[float, float]: """ Gets gets location of a site in latitude and longitude @@ -499,18 +299,16 @@ def get_location_lat_long( :rtype: Tuple(float,float) """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_location().to_latlon() - if isinstance(site, node.Node): - return site.location.to_latlon() - return self.get_topology_site(site).location.to_latlon() + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_location_lat_long() except Exception as e: # logging.warning(f"Failed to get location postal {site}") return 0, 0 - def get_location_postal( - self, site: str or node.Node or network_node.NodeSliver - ) -> str: + def get_location_postal(self, site: str or node.Node) -> str: """ Gets the location of a site by postal address @@ -520,20 +318,18 @@ def get_location_postal( :rtype: String """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_location().postal - if isinstance(site, node.Node): - return site.location.postal - return self.get_topology_site(site).location.postal + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_location_postal() except Exception as e: # logging.debug(f"Failed to get location postal {site}") return "" - def get_host_capacity( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_host_capacity(self, site: str or node.Node) -> int: """ - Gets the number of worker hosts at the site + Gets the number of hosts at the site :param site: site name or site object :type site: String or Node or NodeSliver @@ -541,18 +337,16 @@ def get_host_capacity( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacities().unit - if isinstance(site, node.Node): - return site.capacities.unit - return self.get_topology_site(site).capacities.unit + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_host_capacity() except Exception as e: # logging.debug(f"Failed to get host count {site}") return 0 - def get_cpu_capacity( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_cpu_capacity(self, site: str or node.Node) -> int: """ Gets the total number of cpus at the site @@ -562,18 +356,16 @@ def get_cpu_capacity( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacities().cpu - if isinstance(site, node.Node): - return site.capacities.cpu - return self.get_topology_site(site).capacities.cpu + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_cpu_capacity() except Exception as e: # logging.debug(f"Failed to get cpu capacity {site}") return 0 - def get_core_capacity( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_core_capacity(self, site: str or node.Node) -> int: """ Gets the total number of cores at the site @@ -583,18 +375,16 @@ def get_core_capacity( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacities().core - if isinstance(site, node.Node): - return site.capacities.core - return self.get_topology_site(site).capacities.core + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_core_capacity() except Exception as e: # logging.debug(f"Failed to get core capacity {site}") return 0 - def get_core_allocated( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_core_allocated(self, site: str or node.Node) -> int: """ Gets the number of currently allocated cores at the site @@ -604,18 +394,16 @@ def get_core_allocated( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacity_allocations().core - if isinstance(site, node.Node): - return site.capacity_allocations.core - return self.get_topology_site(site).capacity_allocations.core + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_core_allocated() except Exception as e: # logging.debug(f"Failed to get cores allocated {site}") return 0 - def get_core_available( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_core_available(self, site: str or node.Node) -> int: """ Gets the number of currently available cores at the site @@ -625,14 +413,16 @@ def get_core_available( :rtype: int """ try: - return self.get_core_capacity(site) - self.get_core_allocated(site) + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_core_available() except Exception as e: # logging.debug(f"Failed to get cores available {site}") return self.get_core_capacity(site) - def get_ram_capacity( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_ram_capacity(self, site: str or node.Node) -> int: """ Gets the total amount of memory at the site in GB @@ -642,18 +432,16 @@ def get_ram_capacity( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacities().ram - if isinstance(site, node.Node): - return site.capacities.ram - return self.get_topology_site(site).capacities.ram + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_ram_capacity() except Exception as e: # logging.debug(f"Failed to get ram capacity {site}") return 0 - def get_ram_allocated( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_ram_allocated(self, site: str or node.Node) -> int: """ Gets the amount of memory currently allocated the site in GB @@ -663,18 +451,16 @@ def get_ram_allocated( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacity_allocations().ram - if isinstance(site, node.Node): - return site.capacity_allocations.ram - return self.get_topology_site(site).capacity_allocations.ram + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_ram_allocated() except Exception as e: # logging.debug(f"Failed to get ram allocated {site}") return 0 - def get_ram_available( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_ram_available(self, site: str or node.Node) -> int: """ Gets the amount of memory currently available the site in GB @@ -684,14 +470,16 @@ def get_ram_available( :rtype: int """ try: - return self.get_ram_capacity(site) - self.get_ram_allocated(site) + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_ram_available() except Exception as e: # logging.debug(f"Failed to get ram available {site_name}") return self.get_ram_capacity(site) - def get_disk_capacity( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_disk_capacity(self, site: str or node.Node) -> int: """ Gets the total amount of disk available the site in GB @@ -701,18 +489,16 @@ def get_disk_capacity( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacities().disk - if isinstance(site, node.Node): - return site.capacities.disk - return self.get_topology_site(site).capacities.disk + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_disk_capacity() except Exception as e: # logging.debug(f"Failed to get disk capacity {site}") return 0 - def get_disk_allocated( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_disk_allocated(self, site: str or node.Node) -> int: """ Gets the amount of disk allocated the site in GB @@ -722,18 +508,16 @@ def get_disk_allocated( :rtype: int """ try: - if isinstance(site, network_node.NodeSliver): - return site.get_capacity_allocations().disk - if isinstance(site, node.Node): - return site.capacity_allocations.disk - return self.get_topology_site(site).capacity_allocations.disk + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_disk_allocated() except Exception as e: # logging.debug(f"Failed to get disk allocated {site}") return 0 - def get_disk_available( - self, site: str or node.Node or network_node.NodeSliver - ) -> int: + def get_disk_available(self, site: str or node.Node) -> int: """ Gets the amount of disk available the site in GB @@ -743,14 +527,16 @@ def get_disk_available( :rtype: int """ try: - return self.get_disk_capacity(site) - self.get_disk_allocated(site) + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_disk_available() except Exception as e: # logging.debug(f"Failed to get disk available {site_name}") return self.get_disk_capacity(site) - def get_ptp_capable( - self, site: str or node.Node or network_node.NodeSliver - ) -> bool: + def get_ptp_capable(self, site: str or node.Node) -> bool: """ Gets the PTP flag of the site - if it has a native PTP capability :param site: site name or object @@ -759,16 +545,22 @@ def get_ptp_capable( :rtype: bool """ try: - if isinstance(site, network_node.NodeSliver): - return site.flags.ptp - if isinstance(site, node.Node): - return site.flags.ptp - return self.get_topology_site(site).flags.ptp + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.get_ptp_capable() except Exception as e: # logging.debug(f"Failed to get PTP status for {site}") return False def get_fablib_manager(self): + """ + Get the Fabric library manager associated with the resources. + + :return: The Fabric library manager. + :rtype: Any + """ return self.fablib_manager def update( @@ -820,9 +612,25 @@ def update( self.topology = topology + for site_name, site in self.topology.sites.items(): + s = Site(site=site, fablib_manager=self.get_fablib_manager()) + self.sites[site_name] = s + def get_topology(self, update: bool = False) -> AdvertisedTopology: """ - Not intended for API use + Get the FIM object of the Resources. + + :return: The FIM of the resources. + :rtype: AdvertisedTopology + """ + return self.get_fim() + + def get_fim(self, update: bool = False) -> AdvertisedTopology: + """ + Get the FIM object of the Resources. + + :return: The FIM of the resources. + :rtype: AdvertisedTopology """ if update or self.topology is None: self.update() @@ -866,87 +674,38 @@ def get_link_list(self, update: bool = False) -> List[str]: return rtn_links def site_to_json(self, site, latlon=True): + """ + Convert site information into a JSON string. + + :param site: Name of the site or site object. + :type site: str or node.Node + + :param latlon: Flag indicating whether to convert address to latitude and longitude. + :type latlon: bool + + :return: JSON string representation of the site information. + :rtype: str + """ return json.dumps(self.site_to_dict(site, latlon=latlon), indent=4) - def site_to_dict( - self, site: str or node.Node or network_node.NodeSliver, latlon=True - ): + def site_to_dict(self, site: str or node.Node, latlon=True): """ - Convert site information into a dictionary + Convert site information into a dictionary. - :param site: site name or site object - :param latlon: convert address to latlon (makes online call to openstreetmaps.org) - """ - site_info = self.get_site_info(site) - d = { - "name": site.name if isinstance(site, node.Node) else site.get_name(), - "state": self.get_state(site), - "address": self.get_location_postal(site), - "location": self.get_location_lat_long(site) if latlon else "", - "ptp_capable": self.get_ptp_capable(site), - "hosts": self.get_host_capacity(site), - "cpus": self.get_cpu_capacity(site), - } + :param site: Name of the site or site object. + :type site: str or node.Node - for attribute, names in self.site_attribute_name_mappings.items(): - capacity = site_info.get(attribute, {}).get(self.CAPACITY.lower(), 0) - allocated = site_info.get(attribute, {}).get(self.ALLOCATED.lower(), 0) - available = capacity - allocated - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.AVAILABLE.lower()}"] = available - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.CAPACITY.lower()}"] = capacity - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.ALLOCATED.lower()}"] = allocated - - if not latlon: - d.pop("location") - - return d - - def site_to_dictXXX(self, site): - site_name = site.name - site_info = self.get_site_info(site) - d = { - "name": {self.PRETTY_NAME: "Name", self.VALUE: site.name}, - "address": { - self.PRETTY_NAME: "Address", - self.VALUE: self.get_location_postal(site_name), - }, - "location": { - self.PRETTY_NAME: "Location", - self.VALUE: self.get_location_lat_long(site_name), - }, - "ptp": { - self.PRETTY_NAME: "PTP Capable", - self.VALUE: self.get_ptp_capable(site), - }, - self.HOSTS.lower(): { - self.PRETTY_NAME: self.HOSTS, - self.VALUE: self.get_host_capacity(site_name), - }, - self.CPUS.lower(): { - self.PRETTY_NAME: self.CPUS, - self.VALUE: self.get_cpu_capacity(site_name), - }, - } + :param latlon: Flag indicating whether to convert address to latitude and longitude. + :type latlon: bool - for attribute, names in self.site_attribute_name_mappings.items(): - capacity = site_info.get(attribute, {}).get(self.CAPACITY.lower(), 0) - allocated = site_info.get(attribute, {}).get(self.ALLOCATED.lower(), 0) - available = capacity - allocated - - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.AVAILABLE.lower()}"] = { - self.PRETTY_NAME: f"{names.get(self.PRETTY_NAME)} {self.AVAILABLE}", - self.VALUE: available, - } - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.CAPACITY.lower()}"] = { - self.PRETTY_NAME: f"{names.get(self.PRETTY_NAME)} {self.CAPACITY}", - self.VALUE: capacity, - } - d[f"{names.get(self.NON_PRETTY_NAME)}_{self.ALLOCATED.lower()}"] = { - self.PRETTY_NAME: f"{names.get(self.PRETTY_NAME)} {self.ALLOCATED}", - self.VALUE: allocated, - } - - return d + :return: Dictionary representation of the site information. + :rtype: dict + """ + if isinstance(site, str): + site = self.get_site(site_name=site) + elif isinstance(site, node.Node): + site = Site(site=site, fablib_manager=self.fablib_manager) + return site.to_dict() def list_sites( self, @@ -957,14 +716,38 @@ def list_sites( pretty_names=True, latlon=True, ): + """ + List information about sites. + + :param output: Output type for listing information. + :type output: Any, optional + + :param fields: List of fields to include in the output. + :type fields: Optional[List[str]], optional + + :param quiet: Flag indicating whether to display output quietly. + :type quiet: bool, optional + + :param filter_function: Function to filter the output. + :type filter_function: Optional[Callable[[Dict], bool]], optional + + :param pretty_names: Flag indicating whether to use pretty names for fields. + :type pretty_names: bool, optional + + :param latlon: Flag indicating whether to convert address to latitude and longitude. + :type latlon: bool, optional + + :return: Table listing information about sites. + :rtype: str + """ table = [] - for site_name, site in self.topology.sites.items(): - site_dict = self.site_to_dict(site, latlon=latlon) + for site_name, site in self.sites.items(): + site_dict = site.to_dict() if site_dict.get("hosts"): table.append(site_dict) if pretty_names: - pretty_names_dict = self.site_pretty_names + pretty_names_dict = ResourceConstants.pretty_names else: pretty_names_dict = {} @@ -978,6 +761,56 @@ def list_sites( pretty_names_dict=pretty_names_dict, ) + def list_hosts( + self, + output=None, + fields=None, + quiet=False, + filter_function=None, + pretty_names=True, + ): + """ + List information about hosts. + + :param output: Output type for listing information. + :type output: Any, optional + + :param fields: List of fields to include in the output. + :type fields: Optional[List[str]], optional + + :param quiet: Flag indicating whether to display output quietly. + :type quiet: bool, optional + + :param filter_function: Function to filter the output. + :type filter_function: Optional[Callable[[Dict], bool]], optional + + :param pretty_names: Flag indicating whether to use pretty names for fields. + :type pretty_names: bool, optional + + :return: Table listing information about hosts. + :rtype: str + """ + table = [] + for site_name, site in self.sites.items(): + for host_name, host in site.get_hosts().items(): + host_dict = host.to_dict() + table.append(host_dict) + + if pretty_names: + pretty_names_dict = ResourceConstants.pretty_names + else: + pretty_names_dict = {} + + return self.get_fablib_manager().list_table( + table, + fields=fields, + title="Hosts", + output=output, + quiet=quiet, + filter_function=filter_function, + pretty_names_dict=pretty_names_dict, + ) + class Links(Resources): link_pretty_names = { @@ -1193,7 +1026,22 @@ def list_facility_ports( """ Print a table of link resources in pretty format. - :return: formatted table of resources + :param output: Output type for listing information. + :type output: Any, optional + + :param fields: List of fields to include in the output. + :type fields: Optional[List[str]], optional + + :param quiet: Flag indicating whether to display output quietly. + :type quiet: bool, optional + + :param filter_function: Function to filter the output. + :type filter_function: Optional[Callable[[Dict], bool]], optional + + :param pretty_names: Flag indicating whether to use pretty names for fields. + :type pretty_names: bool, optional + + :return: Formatted table of resources. :rtype: object """ table = [] diff --git a/fabrictestbed_extensions/fablib/site.py b/fabrictestbed_extensions/fablib/site.py new file mode 100644 index 00000000..e1843819 --- /dev/null +++ b/fabrictestbed_extensions/fablib/site.py @@ -0,0 +1,1233 @@ +#!/usr/bin/env python3 +# MIT License +# +# Copyright (c) 2020 FABRIC Testbed +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Author: Komal Thareja(kthar10@renci.org) + +from __future__ import annotations + +import json +import logging +import traceback +from typing import Dict, List, Tuple + +from fabrictestbed.slice_editor import Capacities +from fim.user import Component, node +from fim.view_only_dict import ViewOnlyDict + +from fabrictestbed_extensions.fablib.constants import Constants + + +class ResourceConstants: + attribute_name_mappings = { + Constants.CORES.lower(): { + Constants.NON_PRETTY_NAME: Constants.CORES.lower(), + Constants.PRETTY_NAME: Constants.CORES, + Constants.HEADER_NAME: Constants.CORES, + }, + Constants.RAM.lower(): { + Constants.NON_PRETTY_NAME: Constants.RAM.lower(), + Constants.PRETTY_NAME: Constants.RAM, + Constants.HEADER_NAME: f"{Constants.RAM} ({Capacities.UNITS[Constants.RAM.lower()]})", + }, + Constants.DISK: { + Constants.NON_PRETTY_NAME: Constants.DISK.lower(), + Constants.PRETTY_NAME: Constants.DISK, + Constants.HEADER_NAME: f"{Constants.DISK} ({Capacities.UNITS[Constants.DISK.lower()]})", + }, + Constants.NIC_SHARED_CONNECTX_6: { + Constants.NON_PRETTY_NAME: "nic_basic", + Constants.PRETTY_NAME: "Basic NIC", + Constants.HEADER_NAME: "Basic (100 Gbps NIC)", + }, + Constants.P4_SWITCH: { + Constants.NON_PRETTY_NAME: Constants.P4_SWITCH.lower(), + Constants.PRETTY_NAME: Constants.P4_SWITCH, + Constants.HEADER_NAME: Constants.P4_SWITCH, + }, + Constants.SMART_NIC_CONNECTX_6: { + Constants.NON_PRETTY_NAME: "nic_connectx_6", + Constants.PRETTY_NAME: "ConnectX-6", + Constants.HEADER_NAME: "ConnectX-6 (100 Gbps x2 NIC)", + }, + Constants.SMART_NIC_CONNECTX_5: { + Constants.NON_PRETTY_NAME: "nic_connectx_5", + Constants.PRETTY_NAME: "ConnectX-5", + Constants.HEADER_NAME: "ConnectX-5 (25 Gbps x2 NIC)", + }, + Constants.NVME_P4510: { + Constants.NON_PRETTY_NAME: "nvme", + Constants.PRETTY_NAME: "NVMe", + Constants.HEADER_NAME: "P4510 (NVMe 1TB)", + }, + Constants.GPU_TESLA_T4: { + Constants.NON_PRETTY_NAME: "tesla_t4", + Constants.PRETTY_NAME: "Tesla T4", + Constants.HEADER_NAME: "Tesla T4 (GPU)", + }, + Constants.GPU_RTX6000: { + Constants.NON_PRETTY_NAME: "rtx6000", + Constants.PRETTY_NAME: "RTX6000", + Constants.HEADER_NAME: "RTX6000 (GPU)", + }, + Constants.GPU_A30: { + Constants.NON_PRETTY_NAME: "a30", + Constants.PRETTY_NAME: "A30", + Constants.HEADER_NAME: "A30 (GPU)", + }, + Constants.GPU_A40: { + Constants.NON_PRETTY_NAME: "a40", + Constants.PRETTY_NAME: "A40", + Constants.HEADER_NAME: "A40 (GPU)", + }, + Constants.FPGA_XILINX_U280: { + Constants.NON_PRETTY_NAME: "fpga_u280", + Constants.PRETTY_NAME: "U280", + Constants.HEADER_NAME: "FPGA-Xilinx-U280", + }, + } + pretty_names = { + "name": "Name", + "state": "State", + "address": "Address", + "location": "Location", + "ptp_capable": "PTP Capable", + Constants.HOSTS.lower(): Constants.HOSTS, + Constants.CPUS.lower(): Constants.CPUS, + } + for attribute, names in attribute_name_mappings.items(): + pretty_name = names.get(Constants.PRETTY_NAME) + non_pretty_name = names.get(Constants.NON_PRETTY_NAME) + if pretty_name not in pretty_names: + pretty_names[f"{non_pretty_name}_{Constants.AVAILABLE.lower()}"] = ( + f"{pretty_name} {Constants.AVAILABLE}" + ) + pretty_names[f"{non_pretty_name}_{Constants.ALLOCATED.lower()}"] = ( + f"{pretty_name} {Constants.ALLOCATED}" + ) + pretty_names[f"{non_pretty_name}_{Constants.CAPACITY.lower()}"] = ( + f"{pretty_name} {Constants.CAPACITY}" + ) + + +class Switch: + def __init__(self, switch: node.Node, fablib_manager): + """ + Initialize a Switch object. + + :param switch: The node representing the switch. + :type switch: node.Node + + :param fablib_manager: The manager for the Fabric library. + :type fablib_manager: Any + + """ + self.switch = switch + self.fablib_manager = fablib_manager + + def get_capacity(self) -> int: + """ + Get the capacity of the switch. + + :return: The capacity of the switch. + :rtype: int + """ + try: + return self.switch.capacities.unit + except Exception: + return 0 + + def get_allocated(self) -> int: + """ + Get the allocated capacity of the switch. + + :return: The allocated capacity of the switch. + :rtype: int + """ + try: + return self.switch.capacity_allocations.unit + except Exception: + return 0 + + def get_available(self) -> int: + """ + Get the available capacity of the switch. + + :return: The available capacity of the switch. + :rtype: int + """ + return self.get_capacity() - self.get_allocated() + + def get_fim(self) -> node.Node: + """ + Get the FIM object of the Switch. + + :return: The FIM of the Switch. + :rtype: node.Node + """ + return self.switch + + +class Host: + def __init__(self, host: node.Node, state: str, ptp: bool, fablib_manager): + """ + Initialize a Host object. + + :param host: The node representing the host. + :type host: node.Node + + :param state: The state of the host. + :type state: str + + :param ptp: Boolean indicating if the host is PTP capable. + :type ptp: bool + + :param fablib_manager: The manager for the Fabric library. + :type fablib_manager: Any + + :return: None + """ + self.host = host + self.state = state + self.ptp = ptp + self.fablib_manager = fablib_manager + self.host_info = {} + self.__load() + + def get_fablib_manager(self): + """ + Get the Fabric library manager associated with the host. + + :return: The Fabric library manager. + :rtype: Any + """ + return self.fablib_manager + + def __str__(self): + """ + Convert the Host object to a string representation in JSON format. + + :return: JSON string representation of the Host object. + :rtype: str + """ + return self.to_json() + + def to_dict(self) -> dict: + """ + Convert the Host object to a dictionary. + + :return: Dictionary representation of the Host object. + :rtype: dict + """ + d = { + "name": self.get_name(), + "state": self.get_state(), + "address": self.get_location_postal(), + "location": self.get_location_lat_long(), + "ptp_capable": self.get_ptp_capable(), + } + + for attribute, names in ResourceConstants.attribute_name_mappings.items(): + if attribute in Constants.P4_SWITCH: + continue + capacity = self.host_info.get(attribute.lower(), {}).get( + Constants.CAPACITY.lower(), 0 + ) + allocated = self.host_info.get(attribute.lower(), {}).get( + Constants.ALLOCATED.lower(), 0 + ) + available = capacity - allocated + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.AVAILABLE.lower()}" + ] = available + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.CAPACITY.lower()}" + ] = capacity + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.ALLOCATED.lower()}" + ] = allocated + + return d + + def to_json(self) -> str: + """ + Convert the Host object to a JSON string. + + :return: JSON string representation of the Host object. + :rtype: str + """ + return json.dumps(self.to_dict(), indent=4) + + def get_state(self) -> str: + """ + Get the state of the host. + + :return: The state of the host. + :rtype: str + """ + if not self.state: + return "" + return self.state + + def get_fim(self) -> node.Node: + """ + Get the FIM object of the host. + + :return: The FIM of the host. + :rtype: node.Node + """ + return self.host + + def __load(self): + """ + Load information about the host. + + :return: None + """ + try: + self.host_info[Constants.CORES.lower()] = { + Constants.CAPACITY.lower(): self.get_core_capacity(), + Constants.ALLOCATED.lower(): self.get_core_allocated(), + } + self.host_info[Constants.RAM.lower()] = { + Constants.CAPACITY.lower(): self.get_ram_capacity(), + Constants.ALLOCATED.lower(): self.get_ram_allocated(), + } + self.host_info[Constants.DISK.lower()] = { + Constants.CAPACITY.lower(): self.get_disk_capacity(), + Constants.ALLOCATED.lower(): self.get_disk_allocated(), + } + + if self.host.components: + for component_model_name, c in self.host.components.items(): + comp_cap = self.host_info.setdefault( + component_model_name.lower(), {} + ) + comp_cap.setdefault(Constants.CAPACITY.lower(), 0) + comp_cap.setdefault(Constants.ALLOCATED.lower(), 0) + comp_cap[Constants.CAPACITY.lower()] += c.capacities.unit + if c.capacity_allocations: + comp_cap[ + Constants.ALLOCATED.lower() + ] += c.capacity_allocations.unit + except Exception as e: + # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") + pass + + def get_components(self) -> ViewOnlyDict: + """ + Get the components associated with the host. + + :return: Dictionary-like view of the components associated with the host. + :rtype: ViewOnlyDict + """ + try: + return self.host.components + except Exception as e: + pass + + def get_component(self, comp_model_type: str) -> Component: + """ + Get a specific component associated with the host. + + :param comp_model_type: The type of component to retrieve. + :type comp_model_type: str + + :return: The specified component. + :rtype: Component + """ + try: + return self.host.components.get(comp_model_type) + except Exception as e: + pass + + def show( + self, + output: str = None, + fields: list[str] = None, + quiet: bool = False, + pretty_names=True, + ) -> str: + """ + Creates a tabulated string of all the available resources at a specific host. + + Intended for printing available resources at a host. + + :param output: Output type + :type output: str + :param fields: List of fields to include + :type fields: List + :param quiet: flag indicating verbose or quiet display + :type quiet: bool + :param pretty_names: flag indicating if pretty names for the fields to be used or not + :type pretty_names: bool + + :return: Tabulated string of available resources + :rtype: String + """ + + data = self.to_dict() + + if pretty_names: + pretty_names_dict = ResourceConstants.pretty_names + else: + pretty_names_dict = {} + + host_table = self.get_fablib_manager().show_table( + data, + fields=fields, + title="Host", + output=output, + quiet=quiet, + pretty_names_dict=pretty_names_dict, + ) + + return host_table + + def get_location_postal(self) -> str: + """ + Gets the location of a site by postal address + + :param site: site name or site object + :type site: String or Node or NodeSliver + :return: postal address of the site + :rtype: String + """ + try: + return self.host.location.postal + except Exception as e: + # logging.debug(f"Failed to get postal address for {site}") + return "" + + def get_location_lat_long(self) -> Tuple[float, float]: + """ + Gets gets location of a site in latitude and longitude + + :return: latitude and longitude of the site + :rtype: Tuple(float,float) + """ + try: + return self.host.location.to_latlon() + except Exception as e: + # logging.debug(f"Failed to get latitude and longitude for {site}") + return 0, 0 + + def get_ptp_capable(self) -> bool: + """ + Gets the PTP flag of the site - if it has a native PTP capability + :param site: site name or object + :type site: String or Node or NodeSliver + :return: boolean flag + :rtype: bool + """ + try: + return self.ptp + except Exception as e: + # logging.debug(f"Failed to get PTP status for {site}") + return False + + def get_name(self): + """ + Gets the host name + + :return: str + """ + try: + return self.host.name + except Exception as e: + # logging.debug(f"Failed to get name for {host}") + return "" + + def get_core_capacity(self) -> int: + """ + Gets the total number of cores at the site + + :return: core count + :rtype: int + """ + try: + return self.host.capacities.core + except Exception as e: + # logging.debug(f"Failed to get core capacity {site}") + return 0 + + def get_core_allocated(self) -> int: + """ + Gets the number of currently allocated cores at the site + + :return: core count + :rtype: int + """ + try: + return self.host.capacity_allocations.core + except Exception as e: + # logging.debug(f"Failed to get cores allocated {site}") + return 0 + + def get_core_available(self) -> int: + """ + Gets the number of currently available cores at the site + :return: core count + :rtype: int + """ + try: + return self.get_core_capacity() - self.get_core_allocated() + except Exception as e: + # logging.debug(f"Failed to get cores available {site}") + return self.get_core_capacity() + + def get_ram_capacity(self) -> int: + """ + Gets the total amount of memory at the site in GB + + :return: ram in GB + :rtype: int + """ + try: + return self.host.capacities.ram + except Exception as e: + # logging.debug(f"Failed to get ram capacity {site}") + return 0 + + def get_ram_allocated(self) -> int: + """ + Gets the amount of memory currently allocated the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: ram in GB + :rtype: int + """ + try: + return self.host.capacity_allocations.ram + except Exception as e: + # logging.debug(f"Failed to get ram allocated {site}") + return 0 + + def get_ram_available(self) -> int: + """ + Gets the amount of memory currently available the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: ram in GB + :rtype: int + """ + try: + return self.get_ram_capacity() - self.get_ram_allocated() + except Exception as e: + # logging.debug(f"Failed to get ram available {site_name}") + return self.get_ram_capacity() + + def get_disk_capacity(self) -> int: + """ + Gets the total amount of disk available the site in GB + + :return: disk in GB + :rtype: int + """ + try: + return self.host.capacities.disk + except Exception as e: + # logging.debug(f"Failed to get disk capacity {site}") + return 0 + + def get_disk_allocated(self) -> int: + """ + Gets the amount of disk allocated the site in GB + + :return: disk in GB + :rtype: int + """ + try: + return self.host.capacity_allocations.disk + except Exception as e: + # logging.debug(f"Failed to get disk allocated {site}") + return 0 + + def get_disk_available(self) -> int: + """ + Gets the amount of disk available the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: disk in GB + :rtype: int + """ + try: + return self.get_disk_capacity() - self.get_disk_allocated() + except Exception as e: + # logging.debug(f"Failed to get disk available {site_name}") + return self.get_disk_capacity() + + def get_component_capacity( + self, + component_model_name: str, + ) -> int: + """ + Gets the total site capacity of a component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: total component capacity + :rtype: int + """ + component_capacity = 0 + try: + if component_model_name in self.host.components: + component_capacity += self.host.components[ + component_model_name + ].capacities.unit + return component_capacity + except Exception as e: + # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") + return component_capacity + + def get_component_allocated( + self, + component_model_name: str, + ) -> int: + """ + Gets gets number of currently allocated components on a the site + by the component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: currently allocated component of this model + :rtype: int + """ + component_allocated = 0 + try: + if ( + component_model_name in self.host.components + and self.host.components[component_model_name].capacity_allocations + ): + component_allocated += self.host.components[ + component_model_name + ].capacity_allocations.unit + return component_allocated + except Exception as e: + # logging.error(f"Failed to get {component_model_name} allocated {site}: {e}") + return component_allocated + + def get_component_available( + self, + component_model_name: str, + ) -> int: + """ + Gets gets number of currently available components on the site + by the component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: currently available component of this model + :rtype: int + """ + try: + return self.get_component_capacity( + component_model_name + ) - self.get_component_allocated(component_model_name) + except Exception as e: + # logging.debug(f"Failed to get {component_model_name} available {site}") + return self.get_component_capacity(component_model_name) + + +class Site: + def __init__(self, site: node.Node, fablib_manager): + """ + Initialize a Site object. + + :param site: The node representing the site. + :type site: node.Node + + :param fablib_manager: The manager for the Fabric library. + :type fablib_manager: Any + + :return: None + """ + super().__init__() + self.site = site + self.fablib_manager = fablib_manager + self.hosts = {} + self.switches = {} + self.site_info = {} + self.__load() + + def get_hosts(self) -> Dict[str, Host]: + """ + Get the hosts associated with the site. + + :return: Dictionary of hosts associated with the site. + :rtype: Dict[str, Host] + """ + return self.hosts + + def __load(self): + """ + Load information about the site. + + :return: None + """ + self.__load_hosts() + self.__load_site_info() + + def __load_hosts(self): + """ + Load Hosts and Switches for a site. + + :return: None + """ + try: + from fim.graph.abc_property_graph import ABCPropertyGraph + + node_id_list = self.site.topo.graph_model.get_first_neighbor( + node_id=self.site.node_id, + rel=ABCPropertyGraph.REL_HAS, + node_label=ABCPropertyGraph.CLASS_NetworkNode, + ) + for nid in node_id_list: + _, node_props = self.site.topo.graph_model.get_node_properties( + node_id=nid + ) + n = node.Node( + name=node_props[ABCPropertyGraph.PROP_NAME], + node_id=nid, + topo=self.site.topo, + ) + # exclude Facility nodes + from fim.user import NodeType + + if n.type == NodeType.Server: + self.hosts[n.name] = Host( + host=n, + state=self.get_state(n.name), + ptp=self.get_ptp_capable(), + fablib_manager=self.fablib_manager, + ) + elif n.type == NodeType.Switch: + self.switches[n.name] = Switch( + switch=n, fablib_manager=self.get_fablib_manager() + ) + except Exception as e: + logging.error(f"Error occurred - {e}") + logging.error(traceback.format_exc()) + + def to_json(self) -> str: + """ + Convert the Site object to a JSON string. + + :return: JSON string representation of the Site object. + :rtype: str + """ + return json.dumps(self.to_dict(), indent=4) + + def get_fablib_manager(self): + """ + Get the Fabric library manager associated with the site. + + :return: The Fabric library manager. + :rtype: Any + """ + return self.fablib_manager + + def to_row(self) -> Tuple[list, list]: + """ + Convert the Site object to a row for tabular display. + + :return: Tuple containing headers and row for tabular display. + :rtype: Tuple[list, list] + """ + headers = [ + "Name", + "PTP Capable", + Constants.CPUS, + ] + row = [ + self.get_name(), + self.get_ptp_capable(), + self.get_cpu_capacity(), + ] + + for attribute, names in ResourceConstants.attribute_name_mappings.items(): + allocated = self.site_info.get(attribute, {}).get( + Constants.ALLOCATED.lower(), 0 + ) + capacity = self.site_info.get(attribute, {}).get( + Constants.CAPACITY.lower(), 0 + ) + available = capacity - allocated + row.append(f"{available}/{capacity}") + headers.append(names.get(Constants.HEADER_NAME)) + return headers, row + + def get_host(self, name: str) -> Host: + """ + Get a specific host associated with the site. + + :param name: The name of the host to retrieve. + :type name: str + + :return: The specified host. + :rtype: Host + """ + return self.hosts.get(name) + + def __str__(self) -> str: + """ + Convert the Site object to a string representation in JSON format. + + :return: JSON string representation of the Site object. + :rtype: str + """ + return self.to_json() + + def get_name(self) -> str: + """ + Gets the site name + + :return: str(MaintenanceState) + """ + try: + return self.site.name + except Exception as e: + # logging.debug(f"Failed to get name for {site}") + return "" + + def get_state(self, host: str = None): + """ + Gets the maintenance state of the node + + :return: str(MaintenanceState) + """ + try: + if not host: + return str(self.site.maintenance_info.get(self.site.name).state) + else: + if self.site.maintenance_info.get(host): + return str(self.site.maintenance_info.get(host).state) + else: + return "Active" + except Exception as e: + # logging.debug(f"Failed to get maintenance state for {site}") + return "" + + def get_location_postal(self) -> str: + """ + Gets the location of a site by postal address + + :param site: site name or site object + :type site: String or Node or NodeSliver + :return: postal address of the site + :rtype: String + """ + try: + return self.site.location.postal + except Exception as e: + # logging.debug(f"Failed to get postal address for {site}") + return "" + + def get_location_lat_long(self) -> Tuple[float, float]: + """ + Gets gets location of a site in latitude and longitude + + :return: latitude and longitude of the site + :rtype: Tuple(float,float) + """ + try: + return self.site.location.to_latlon() + except Exception as e: + # logging.debug(f"Failed to get latitude and longitude for {site}") + return 0, 0 + + def get_ptp_capable(self) -> bool: + """ + Gets the PTP flag of the site - if it has a native PTP capability + :param site: site name or object + :type site: String or Node or NodeSliver + :return: boolean flag + :rtype: bool + """ + try: + return self.site.flags.ptp + except Exception as e: + # logging.debug(f"Failed to get PTP status for {site}") + return False + + def get_host_capacity(self) -> int: + """ + Gets the number of hosts at the site + + :param site: site name or site object + :type site: String or Node or NodeSliver + :return: host count + :rtype: int + """ + try: + return self.site.capacities.unit + except Exception as e: + # logging.debug(f"Failed to get host count {site}") + return 0 + + def get_cpu_capacity(self) -> int: + """ + Gets the total number of cpus at the site + + :param site: site name or site object + :type site: String or node.Node or NodeSliver + :return: cpu count + :rtype: int + """ + try: + return self.site.capacities.cpu + except Exception as e: + # logging.debug(f"Failed to get cpu capacity {site}") + return 0 + + def to_dict(self) -> dict: + """ + Convert site information into a dictionary + """ + d = { + "name": self.get_name(), + "state": self.get_state(), + "address": self.get_location_postal(), + "location": self.get_location_lat_long(), + "ptp_capable": self.get_ptp_capable(), + "hosts": self.get_host_capacity(), + "cpus": self.get_cpu_capacity(), + } + + for attribute, names in ResourceConstants.attribute_name_mappings.items(): + capacity = self.site_info.get(attribute.lower(), {}).get( + Constants.CAPACITY.lower(), 0 + ) + allocated = self.site_info.get(attribute.lower(), {}).get( + Constants.ALLOCATED.lower(), 0 + ) + available = capacity - allocated + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.AVAILABLE.lower()}" + ] = available + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.CAPACITY.lower()}" + ] = capacity + d[ + f"{names.get(Constants.NON_PRETTY_NAME)}_{Constants.ALLOCATED.lower()}" + ] = allocated + + return d + + def __load_site_info(self): + """ + Load the total site capacity of all components for a site + """ + try: + self.site_info[Constants.CORES.lower()] = { + Constants.CAPACITY.lower(): self.get_core_capacity(), + Constants.ALLOCATED.lower(): self.get_core_allocated(), + } + self.site_info[Constants.RAM.lower()] = { + Constants.CAPACITY.lower(): self.get_ram_capacity(), + Constants.ALLOCATED.lower(): self.get_ram_allocated(), + } + self.site_info[Constants.DISK.lower()] = { + Constants.CAPACITY.lower(): self.get_disk_capacity(), + Constants.ALLOCATED.lower(): self.get_disk_allocated(), + } + + for h in self.hosts.values(): + if h.get_components(): + for component_model_name, c in h.get_components().items(): + comp_cap = self.site_info.setdefault( + component_model_name.lower(), {} + ) + comp_cap.setdefault(Constants.CAPACITY.lower(), 0) + comp_cap.setdefault(Constants.ALLOCATED.lower(), 0) + comp_cap[Constants.CAPACITY.lower()] += c.capacities.unit + if c.capacity_allocations: + comp_cap[ + Constants.ALLOCATED.lower() + ] += c.capacity_allocations.unit + + p4_mappings = ResourceConstants.attribute_name_mappings.get( + Constants.P4_SWITCH + ) + for s in self.switches.values(): + self.site_info[p4_mappings.get(Constants.NON_PRETTY_NAME)] = { + Constants.CAPACITY.lower(): s.get_capacity(), + Constants.ALLOCATED.lower(): s.get_allocated(), + } + + except Exception as e: + # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") + pass + + def show( + self, + output: str = None, + fields: list[str] = None, + quiet: bool = False, + pretty_names=True, + ) -> str: + """ + Creates a tabulated string of all the available resources at a specific site. + + Intended for printing available resources at a site. + + :param output: Output type + :type output: str + :param fields: List of fields to include + :type fields: List + :param quiet: flag indicating verbose or quiet display + :type quiet: bool + :param pretty_names: flag indicating if pretty names for the fields to be used or not + :type pretty_names: bool + + :return: Tabulated string of available resources + :rtype: String + """ + + data = self.to_dict() + + if pretty_names: + pretty_names_dict = ResourceConstants.pretty_names + else: + pretty_names_dict = {} + + site_table = self.get_fablib_manager().show_table( + data, + fields=fields, + title="Site", + output=output, + quiet=quiet, + pretty_names_dict=pretty_names_dict, + ) + + return site_table + + def get_component_capacity( + self, + component_model_name: str, + ) -> int: + """ + Gets the total site capacity of a component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: total component capacity + :rtype: int + """ + component_capacity = 0 + try: + for h in self.hosts.values(): + component_capacity += h.get_component_capacity( + component_model_name=component_model_name + ) + return component_capacity + except Exception as e: + # logging.error(f"Failed to get {component_model_name} capacity {site}: {e}") + return component_capacity + + def get_component_allocated( + self, + component_model_name: str, + ) -> int: + """ + Gets gets number of currently allocated components on a the site + by the component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: currently allocated component of this model + :rtype: int + """ + component_allocated = 0 + try: + for h in self.hosts.values(): + component_allocated += h.get_component_allocated( + component_model_name=component_model_name + ) + return component_allocated + except Exception as e: + # logging.error(f"Failed to get {component_model_name} allocated {site}: {e}") + return component_allocated + + def get_component_available( + self, + component_model_name: str, + ) -> int: + """ + Gets gets number of currently available components on the site + by the component by model name. + + :param component_model_name: component model name + :type component_model_name: String + :return: currently available component of this model + :rtype: int + """ + try: + return self.get_component_capacity( + component_model_name + ) - self.get_component_allocated(component_model_name) + except Exception as e: + # logging.debug(f"Failed to get {component_model_name} available {site}") + return self.get_component_capacity(component_model_name) + + def get_fim(self) -> node.Node: + """ + Get the FIM object of the site. + + :return: The FIM of the site. + :rtype: node.Node + """ + return self.site + + def get_core_capacity(self) -> int: + """ + Gets the total number of cores at the site + + :return: core count + :rtype: int + """ + try: + return self.site.capacities.core + except Exception as e: + # logging.debug(f"Failed to get core capacity {site}") + return 0 + + def get_core_allocated(self) -> int: + """ + Gets the number of currently allocated cores at the site + + :return: core count + :rtype: int + """ + try: + return self.site.capacity_allocations.core + except Exception as e: + # logging.debug(f"Failed to get cores allocated {site}") + return 0 + + def get_core_available(self) -> int: + """ + Gets the number of currently available cores at the site + :return: core count + :rtype: int + """ + try: + return self.get_core_capacity() - self.get_core_allocated() + except Exception as e: + # logging.debug(f"Failed to get cores available {site}") + return self.get_core_capacity() + + def get_ram_capacity(self) -> int: + """ + Gets the total amount of memory at the site in GB + + :return: ram in GB + :rtype: int + """ + try: + return self.site.capacities.ram + except Exception as e: + # logging.debug(f"Failed to get ram capacity {site}") + return 0 + + def get_ram_allocated(self) -> int: + """ + Gets the amount of memory currently allocated the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: ram in GB + :rtype: int + """ + try: + return self.site.capacity_allocations.ram + except Exception as e: + # logging.debug(f"Failed to get ram allocated {site}") + return 0 + + def get_ram_available(self) -> int: + """ + Gets the amount of memory currently available the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: ram in GB + :rtype: int + """ + try: + return self.get_ram_capacity() - self.get_ram_allocated() + except Exception as e: + # logging.debug(f"Failed to get ram available {site_name}") + return self.get_ram_capacity() + + def get_disk_capacity(self) -> int: + """ + Gets the total amount of disk available the site in GB + + :return: disk in GB + :rtype: int + """ + try: + return self.site.capacities.disk + except Exception as e: + # logging.debug(f"Failed to get disk capacity {site}") + return 0 + + def get_disk_allocated(self) -> int: + """ + Gets the amount of disk allocated the site in GB + + :return: disk in GB + :rtype: int + """ + try: + return self.site.capacity_allocations.disk + except Exception as e: + # logging.debug(f"Failed to get disk allocated {site}") + return 0 + + def get_disk_available(self) -> int: + """ + Gets the amount of disk available the site in GB + + :param site: site name or object + :type site: String or Node or NodeSliver + :return: disk in GB + :rtype: int + """ + try: + return self.get_disk_capacity() - self.get_disk_allocated() + except Exception as e: + # logging.debug(f"Failed to get disk available {site_name}") + return self.get_disk_capacity() + + def get_host_names(self) -> List[str]: + """ + Gets a list of all currently available hosts + + :return: list of host names + :rtype: List[String] + """ + return list(self.hosts.keys())