Source code for ironic.common.trait_based_networking.plan

#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.


from ironic.common.i18n import _
from ironic.common.trait_based_networking import base
from ironic.conductor.task_manager import TaskManager
from ironic.objects.node import Node
from ironic.objects.port import Port
from ironic.objects.portgroup import Portgroup


from collections.abc import Callable
import itertools


[docs] def filter_out_attached_portlikes( portlikes: list[base.PrimordialPort], actions: list[base.AttachAction]) -> list[base.PrimordialPort]: """Filters out attached portlikes based on generated attach actions""" matched_uuids = set([action.portlike_uuid() for action in actions]) return [portlike for portlike in portlikes if portlike.uuid not in matched_uuids]
[docs] def is_no_match_list(actions: list[base.RenderedAction]) -> bool: """Check if a list contains only a NoMatch action""" return len(actions) == 1 and isinstance(actions[0], base.NoMatch)
[docs] def filter_out_grouped_ports( portlikes: list[base.PrimordialPort], actions: list[base.GroupAndAttachPorts]) -> list[base.PrimordialPort]: """Filters out ports that have been used to form dynamic portgroups""" matched_uuids: set[str] = set() for action in actions: matched_uuids = matched_uuids.union(set(action.port_uuids)) return [portlike for portlike in portlikes if portlike.uuid not in matched_uuids]
[docs] def plan_network( network_trait: base.NetworkTrait, node_uuid: str, node_ports: list[base.Port], node_portgroups: list[base.Portgroup], node_networks: list[base.Network]) -> list[base.RenderedAction]: """Plan the network of a node based on TBN traits :param network_trait: A single NetworkTrait to consider for planning. :param node_uuid: The UUID of the node to which this plan applies. :param ports: A list of Ports available to this node. :param portgroups: A list of Portgroups available to this node. :param node_networks: A list of networks available to this node. :returns: A list of RenderedActions which should be executed by the appropriate network driver. """ rendered_actions = [] # Order ports and portgroups by ID, newest first. node_ports.sort(key=lambda port: port.id, reverse=True) node_portgroups.sort(key=lambda portgroup: portgroup.id, reverse=True) portlikes = [base.Port.from_ironic_port(port) for port in node_ports] portgrouplikes = [base.Portgroup.from_ironic_portgroup(portgroup) for portgroup in node_portgroups] for trait_action in network_trait.actions: new_actions = [] match trait_action.action: case base.Actions.ATTACH_PORT: new_actions = _plan_attach_portlike( trait_action, node_uuid, portlikes, node_networks, 'port', lambda action_args: base.AttachPort(*action_args)) if not is_no_match_list(new_actions): portlikes = filter_out_attached_portlikes(portlikes, new_actions) case base.Actions.ATTACH_PORTGROUP: new_actions = _plan_attach_portlike( trait_action, node_uuid, portgrouplikes, node_networks, 'portgroup', lambda action_args: base.AttachPortgroup(*action_args)) if not is_no_match_list(new_actions): portgrouplikes = filter_out_attached_portlikes( portgrouplikes, new_actions) case base.Actions.GROUP_AND_ATTACH_PORTS: new_actions = _plan_group_and_attach_ports( trait_action, node_uuid, portlikes, node_networks) if not is_no_match_list(new_actions): portlikes = filter_out_grouped_ports( portlikes, new_actions) # TODO(clif): Support bond_ports? case _: new_actions = [base.NotImplementedAction(trait_action.action)] # NOTE(clif): Ordering of actions is up to the operator. rendered_actions.extend(new_actions) return rendered_actions
def _plan_attach_portlike( trait_action: base.NetworkTrait, node_uuid: str, node_portlikes: list[base.PrimordialPort], node_networks: list[base.Network], type_name: str, action_func: Callable[[base.NetworkTrait, str, str, str], base.RenderedAction] ) -> list[base.RenderedAction]: """Mainly called by plan_netwrok to determine which portlikes to attach""" actions = [] for (portlike, network) in itertools.product(node_portlikes, node_networks): if trait_action.matches(portlike, network): actions.append(action_func((trait_action, node_uuid, portlike.uuid, network.id))) # No minimum count means match the first one. if trait_action.min_count is None: break if trait_action.max_count == len(actions): break if len(actions) == 0: return [base.NoMatch(trait_action, node_uuid, _(f"No ({type_name}, network) pairs matched " "rule."))] if (trait_action.min_count is not None and len(actions) < trait_action.min_count): return [base.NoMatch(trait_action, node_uuid, _(f"Not enough ({type_name}, network) pairs " "matched to meet minimum count threshold. " f"Matched {len(actions)} but min_count is " f"{trait_action.min_count}."))] return actions def _plan_group_and_attach_ports( trait_action: base.TraitAction, node_uuid: str, node_ports: list[base.Port], node_networks: list[base.Network]) -> list[base.RenderedAction]: """Called by plan_network to handle group_and_attach_ports actions""" matched_ports: list [base.Port] = [] # NOTE(clif): Valid min_count and max_counts for group_and_attach_ports # actions are enforced at configuration ingestion time. # NOTE(Clif): Only consider ports that allow dynamic_portgrouping. available_ports: list[base.Port] = [ port for port in node_ports if port.available_for_dynamic_portgroup ] for port in available_ports: # NOTE(clif): Network is not considered when grouping ports together. # It will be when deciding which network to attach. if trait_action.matches(port, base.Network.universal_network()): if len(matched_ports) > 0: # NOTE(clif): Once we match a port, all subsequent ports must # have the same physical_network. This is a requirement of # Portgroups. if matched_ports[0].physical_network \ == port.physical_network: matched_ports.append(port) else: matched_ports.append(port) if len(matched_ports) == trait_action.max_count: break if len(matched_ports) < trait_action.min_count: return [base.NoMatch(trait_action, node_uuid, _("Couldn't match enough ports to form a " f"dynamic portgroup for node '{node_uuid}'." "Minimum ports needed is " f"{trait_action.min_count}, and " f"{len(matched_ports)} ports matched."))] # NOTE(clif) Now decide which network we'll attach to. We consider the # first port selected to stand in for the portgroup. for network in node_networks: if trait_action.matches(matched_ports[0], network): return [base.GroupAndAttachPorts( trait_action, node_uuid, [portlike.uuid for portlike in matched_ports], network.id)] # NOTE(clif) If we made it here, no networks matched. return [base.NoMatch(trait_action, node_uuid, _("Enough ports matched to form a dynamic " f"portgroup for node '{node_uuid}'. Unfortunately " "no suitable networks were found to attach."))]
[docs] def all_no_match(actions: list[base.RenderedAction]) -> bool: """Check if a list of actions contains only NoMatch actions""" return all(isinstance(action, base.NoMatch) for action in actions)
[docs] def order_traits(traits: list[base.NetworkTrait]) -> list[base.NetworkTrait]: """Sort a list of traits in ascending trait.order""" return sorted(traits, key=lambda t: t.order)
# TODO(clif): Lifted from ironic.drivers.network.common to break a circular # dependency. Maybe there's a common spot to lift this out to? TENANT_VIF_KEY = 'tenant_vif_port_id'
[docs] def is_portlike_attached(portlike: Port | Portgroup) -> bool: """Check if a portlike is attached or not""" return (portlike.internal_info is not None and portlike.internal_info.get(TENANT_VIF_KEY) is not None)
[docs] def plan_vif_attach(traits: list[base.NetworkTrait], task: TaskManager, vif_info: dict) -> list[base.RenderedAction]: """Main entry point of TBN from _vif_attach_tbn in NeutronVIFPortIDMixIn :param traits: A list of NetworkTraits that apply to the node being considered. :param task: A TaskManager which contains important information about the node and network objects available. :param vif_info: Information about the network (aka vif) which TBN will use to plan actions. :returns: A list of RenderedActions which should be executed by the appropriate network driver. """ # TODO(clif): Take cues from get_free_port_like_object where appropriate. net = base.Network.from_vif_info(vif_info) # Filter out already attached ports and portgroups. free_ports = [port for port in task.ports if not is_portlike_attached(port)] free_portgroups = [pg for pg in task.portgroups if not is_portlike_attached(pg)] for trait in order_traits(traits): actions = plan_network( trait, task.node.uuid, free_ports, free_portgroups, [net]) # If no actions matched, try the next trait. if all_no_match(actions): continue else: return actions # TODO(clif): Maybe this should raise, because vif_attach raises when # it can't find a free port or portgroup to attach. return [base.NoMatch( base.TraitAction( 'plan_vif_attach', base.Actions.ATTACH_PORT, base.FilterExpression.parse("port.category == 'plan_vif_attach'")), task.node.uuid, _("Could not find an applicable port or portgroup to " f"attach to network '{net.id}' in any applicable " "trait."))]
[docs] def filter_traits_for_node(node: Node, traits: list[base.NetworkTrait] ) -> list[base.NetworkTrait]: """Return a list of NetworkTraits that apply to a node""" instance_traits = node.instance_info.get('traits') or [] return [trait for trait in traits if trait.name in set(instance_traits)]