diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b8ca4a..b42f170b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# Unreleased + +### Fixed +- Add Facility Port to allow adding multiple interfaces (Issue [#289](https://github.com/fabric-testbed/fabrictestbed-extensions/issues/289)) + ## [1.6.4] - 2024-03-05 ### Fixed diff --git a/fabrictestbed_extensions/fablib/facility_port.py b/fabrictestbed_extensions/fablib/facility_port.py index e23bb5a1..568fbd5b 100644 --- a/fabrictestbed_extensions/fablib/facility_port.py +++ b/fabrictestbed_extensions/fablib/facility_port.py @@ -32,7 +32,7 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Union import jinja2 from fabrictestbed.slice_editor import Capacities, Labels @@ -41,7 +41,7 @@ from fabrictestbed_extensions.fablib.interface import Interface if TYPE_CHECKING: - from fim.user.interface import Interface as FimInterface + from fim.user.node import Node as FimNode from fabrictestbed_extensions.fablib.slice import Slice @@ -50,7 +50,7 @@ class FacilityPort: fim_interface = None slice = None - def __init__(self, slice: Slice, fim_interface: FimInterface): + def __init__(self, slice: Slice, fim_interface: FimNode): """ :param slice: the fablib slice to have this node on :type slice: Slice @@ -119,7 +119,7 @@ def show( return table - def get_fim_interface(self) -> FimInterface: + def get_fim_interface(self) -> FimNode: return self.fim_interface def get_model(self) -> str: @@ -141,19 +141,35 @@ def new_facility_port( slice: Slice = None, name: str = None, site: str = None, - vlan: str = None, + vlan: Union[str, list] = None, bandwidth: int = 10, ): - fim_facility_port = slice.get_fim_topology().add_facility( - name=name, - site=site, - capacities=Capacities(bw=bandwidth), - labels=Labels(vlan=vlan), - ) + if isinstance(vlan, list): + interfaces = [] + index = 1 + for v in vlan: + iface_tuple = ( + f"iface-{index}", + Labels(vlan=v), + Capacities(bw=bandwidth), + ) + interfaces.append(iface_tuple) + fim_facility_port = slice.get_fim_topology().add_facility( + name=name, + site=site, + interfaces=interfaces, + ) + else: + fim_facility_port = slice.get_fim_topology().add_facility( + name=name, + site=site, + capacities=Capacities(bw=bandwidth), + labels=Labels(vlan=vlan), + ) return FacilityPort(slice, fim_facility_port) @staticmethod - def get_facility_port(slice: Slice = None, facility_port: FimInterface = None): + def get_facility_port(slice: Slice = None, facility_port: FimNode = None): """ :param slice: @@ -195,6 +211,6 @@ def get_interfaces(self) -> List[Interface]: """ ifaces = [] for fim_interface in self.get_fim_interface().interface_list: - ifaces.append(Interface(component=self, fim_interface=fim_interface)) + ifaces.append(Interface(node=self, fim_interface=fim_interface)) return ifaces diff --git a/fabrictestbed_extensions/fablib/interface.py b/fabrictestbed_extensions/fablib/interface.py index a2e7001e..2de0ec62 100644 --- a/fabrictestbed_extensions/fablib/interface.py +++ b/fabrictestbed_extensions/fablib/interface.py @@ -33,7 +33,7 @@ import json import logging from ipaddress import IPv4Address -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Union import jinja2 from fabrictestbed.slice_editor import Flags @@ -44,6 +44,7 @@ from fabrictestbed_extensions.fablib.node import Node from fabrictestbed_extensions.fablib.network_service import NetworkService from fabrictestbed_extensions.fablib.component import Component + from fabrictestbed_extensions.fablib.facility_port import FacilityPort from fabrictestbed.slice_editor import UserData from fim.user.interface import Interface as FimInterface @@ -57,7 +58,12 @@ class Interface: ADDR = "addr" CONFIG = "config" - def __init__(self, component: Component = None, fim_interface: FimInterface = None): + def __init__( + self, + component: Component = None, + fim_interface: FimInterface = None, + node: FacilityPort = None, + ): """ .. note:: @@ -69,12 +75,16 @@ def __init__(self, component: Component = None, fim_interface: FimInterface = No :param fim_interface: the FABRIC information model interface to set on this fablib interface :type fim_interface: FimInterface + + :param node: the facility Port to which interface is assoicated with + :type node: FacilityPort """ super().__init__() self.fim_interface = fim_interface self.component = component self.network = None self.dev = None + self.node = node def get_fablib_manager(self): return self.get_slice().get_fablib_manager() @@ -532,7 +542,7 @@ def get_fim_interface(self) -> FimInterface: """ return self.fim_interface - def get_bandwidth(self) -> str: + def get_bandwidth(self) -> int: """ Gets the bandwidth of an interface. Basic NICs claim 0 bandwidth but are 100 Gbps shared by all Basic NICs on the host. @@ -540,7 +550,7 @@ def get_bandwidth(self) -> str: :return: bandwith :rtype: String """ - if self.get_component().get_model() == "NIC_Basic": + if self.get_component() and self.get_component().get_model() == "NIC_Basic": return 100 else: return self.get_fim_interface().capacities.bw @@ -635,7 +645,10 @@ def get_model(self) -> str: :return: the model of this interface's component :rtype: str """ - return self.get_component().get_model() + if self.node: + return self.node.get_model() + else: + return self.get_component().get_model() def get_site(self) -> str: """ @@ -644,7 +657,7 @@ def get_site(self) -> str: :return: the site this interface is on :rtype: str """ - return self.get_component().get_site() + return self.get_node().get_site() def get_slice(self) -> Slice: """ @@ -655,14 +668,17 @@ def get_slice(self) -> Slice: """ return self.get_node().get_slice() - def get_node(self) -> Node: + def get_node(self) -> Union[Node, FacilityPort]: """ Gets the node this interface's component is on. :return: the node this interface is attached to :rtype: Node """ - return self.get_component().get_node() + if self.node: + return self.node + else: + return self.get_component().get_node() def get_network(self) -> NetworkService: """ @@ -690,8 +706,6 @@ def get_network(self) -> NetworkService: ) return self.network - return None - # fablib.Interface.get_ip_link() def get_ip_link(self): """ @@ -724,14 +738,12 @@ def get_ip_addr_show(self, dev=None): stdout, stderr = self.get_node().execute( f"ip -j addr show {dev}", quiet=True ) + return stdout except Exception as e: - (f"Exception: {e}") logging.error( - f"Failed to get ip addr show info for interface {self.get_name()}" + f"Failed to get ip addr show info for interface {self.get_name()} Exception: {e}" ) - return stdout - # fablib.Interface.get_ip_addr() def get_ip_addr_ssh(self, dev=None): """ diff --git a/fabrictestbed_extensions/fablib/network_service.py b/fabrictestbed_extensions/fablib/network_service.py index b7c26c13..8208cf0d 100644 --- a/fabrictestbed_extensions/fablib/network_service.py +++ b/fabrictestbed_extensions/fablib/network_service.py @@ -46,7 +46,7 @@ from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network import jinja2 -from fabrictestbed.slice_editor import Flags, Labels +from fabrictestbed.slice_editor import Labels from fabrictestbed.slice_editor import NetworkService as FimNetworkService from fabrictestbed.slice_editor import ServiceType, UserData from fim.slivers.network_service import NSLayer, ServiceType @@ -134,14 +134,14 @@ def calculate_l2_nstype(interfaces: List[Interface] = None) -> ServiceType: facility_port_interfaces = 0 for interface in interfaces: sites.add(interface.get_site()) - if isinstance(interface.get_component(), FacilityPort): + if isinstance(interface.get_node(), FacilityPort): includes_facility_port = True facility_port_interfaces += 1 if interface.get_model() == "NIC_Basic": basic_nic_count += 1 rtn_nstype = None - if len(sites) <= 1 and len(sites) >= 0: + if 1 >= len(sites) >= 0: rtn_nstype = NetworkService.network_service_map["L2Bridge"] # elif basic_nic_count == 0 and len(sites) == 2 and len(interfaces) == 2: # #TODO: remove this when STS works on all links. @@ -166,7 +166,7 @@ def calculate_l2_nstype(interfaces: List[Interface] = None) -> ServiceType: @staticmethod def validate_nstype(type, interfaces): """ - Not inteded for API use + Not intended for API use Verifies the network service type against the number of interfaces. @@ -180,6 +180,8 @@ def validate_nstype(type, interfaces): :rtype: bool """ + from fabrictestbed_extensions.fablib.facility_port import FacilityPort + sites = set([]) nics = set([]) nodes = set([]) @@ -197,13 +199,15 @@ def validate_nstype(type, interfaces): if type == NetworkService.network_service_map["L2Bridge"]: if len(sites) > 1: raise Exception( - f"Network type {type} must be empty or include interfaces from exactly one site. {len(sites)} sites requested: {sites}" + f"Network type {type} must be empty or include interfaces from exactly one site. {len(sites)} " + f"sites requested: {sites}" ) elif type == NetworkService.network_service_map["L2PTP"]: if not len(sites) == 2: raise Exception( - f"Network type {type} must include interfaces from exactly two sites. {len(sites)} sites requested: {sites}" + f"Network type {type} must include interfaces from exactly two sites. {len(sites)} sites " + f"requested: {sites}" ) if "NIC_Basic" in nics: raise Exception( @@ -214,20 +218,36 @@ def validate_nstype(type, interfaces): exception_list = [] if len(sites) != 2: exception_list.append( - f"Network type {type} must include interfaces from exactly two sites. {len(sites)} sites requested: {sites}" + f"Network type {type} must include interfaces from exactly two sites. {len(sites)} sites " + f"requested: {sites}" ) if len(interfaces) > 2: hosts = set([]) + nodes_per_site = {} + for interface in interfaces: + node = interface.get_node() + if node.get_site() not in nodes_per_site: + nodes_per_site[node.get_site()] = 0 + if isinstance(node, FacilityPort): + continue + nodes_per_site[node.get_site()] += 1 for interface in interfaces: node = interface.get_node() - if interface.get_model() == "NIC_Basic": - if node.get_host() == None: + if ( + interface.get_model() == "NIC_Basic" + and nodes_per_site[node.get_site()] > 1 + ): + if node.get_host() is None: exception_list.append( - f"Network type {type} does not support multiple NIC_Basic interfaces on VMs residing on the same host. Please see Node.set_host(host_nane) to explicitily bind a nodes to a specific host. Node {node.get_name()} is unbound." + f"Network type {type} does not support multiple NIC_Basic interfaces on VMs " + f"residing on the same host. Please see Node.set_host(host_name) to explicitly " + f"bind a nodes to a specific host. Node {node.get_name()} is unbound." ) elif node.get_host() in hosts: exception_list.append( - f"Network type {type} does not support multiple NIC_Basic interfaces on VMs residing on the same host. Please see Node.set_host(host_nane) to explicitily bind a nodes to a specific host. Multiple nodes bound to {node.get_host()}." + f"Network type {type} does not support multiple NIC_Basic interfaces on VMs residing " + f"on the same host. Please see Node.set_host(host_name) to explicitly bind a nodes " + f"to a specific host. Multiple nodes bound to {node.get_host()}." ) else: hosts.add(node.get_host()) @@ -389,15 +409,15 @@ def new_l2network( vlan1 = interfaces[0].get_vlan() vlan2 = interfaces[1].get_vlan() - if vlan1 == None and vlan2 == None: + if vlan1 is None and vlan2 is None: # TODO: Long term we might have multiple vlan on one property # and will need to make sure they are unique. For now this okay interfaces[0].set_vlan("100") interfaces[1].set_vlan("100") - elif vlan1 == None and vlan2 != None: + elif vlan1 is None and vlan2 is not None: # Match VLANs if one is set. interfaces[0].set_vlan(vlan2) - elif vlan1 != None and vlan2 == None: + elif vlan1 is not None and vlan2 is None: # Match VLANs if one is set. interfaces[1].set_vlan(vlan1) diff --git a/fabrictestbed_extensions/fablib/slice.py b/fabrictestbed_extensions/fablib/slice.py index 9af9cd57..7ccef1af 100644 --- a/fabrictestbed_extensions/fablib/slice.py +++ b/fabrictestbed_extensions/fablib/slice.py @@ -1032,7 +1032,7 @@ def add_l3network( ) def add_facility_port( - self, name: str = None, site: str = None, vlan: str = None + self, name: str = None, site: str = None, vlan: Union[str, list] = None ) -> NetworkService: """ Adds a new L2 facility port to this slice diff --git a/pyproject.toml b/pyproject.toml index caea56a5..7cc44164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "fabrictestbed-extensions" -version = "1.6.4" +version = "1.6.5" description = "FABRIC Python Client Library and CLI Extensions" authors = [ { name = "Paul Ruth", email = "pruth@renci.org" }, @@ -20,7 +20,7 @@ dependencies = [ "ipyleaflet", "ipycytoscape", "tabulate", - "fabrictestbed==1.6.8", + "fabrictestbed==1.6.9", "paramiko", "jinja2>=3.0.0", "pandas",