Skip to content

Commit

Permalink
Migrate Node Network List Functionality Into esisdk
Browse files Browse the repository at this point in the history
Originally, this functionality existed in python-esiclient. We moved
it into esisdk so that multiple projects can reuse the same package,
and to centralize the logic in one place.
  • Loading branch information
ajamias committed Jul 1, 2024
1 parent 8e56bd3 commit 1b06247
Show file tree
Hide file tree
Showing 9 changed files with 1,192 additions and 0 deletions.
Empty file added esi/lib/__init__.py
Empty file.
114 changes: 114 additions & 0 deletions esi/lib/networks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 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.

import concurrent.futures


def get_ports(connection, filter_network=None):
if filter_network:
neutron_ports = connection.network.ports(network_id=filter_network.id)
else:
neutron_ports = connection.network.ports()
return neutron_ports


def network_and_port_list(connection, filter_network=None):
"""Gets accessible networking information
:param connection: An OpenStack connection
:type connection: :class:`~openstack.connection.Connection`
:param filter_network: The name or ID of a network
:returns: A tuple of network ports, networks, floating ips, and port forwardings of the form:
(
[openstack.network.v2.port.Port],
{openstack.network.v2.network.Network.id: openstack.network.v2.network.Network},
{openstack.network.v2.port.Port.id: openstack.network.v2.floating_ip.FloatingIP},
{openstack.network.v2.port.Port.id: openstack.network.v2.port_forwarding.PortForwarding}
)
"""

floating_ips_dict = {}
port_forwardings_dict = {}

with concurrent.futures.ThreadPoolExecutor() as executor:
f1 = executor.submit(get_ports, connection, filter_network)
f2 = executor.submit(connection.network.networks)
f3 = executor.submit(connection.network.ips)
network_ports = list(f1.result())
networks_dict = {network.id: network for network in list(f2.result())}
floating_ips = list(f3.result())

for floating_ip in floating_ips:
# no need to do this for floating IPs associated with a port,
# as port forwarding is irrelevant in such a case
if not floating_ip.port_id:
pfwds = list(connection.network.port_forwardings(floating_ip=floating_ip))
if len(pfwds):
floating_ip.port_id = pfwds[0].internal_port_id
port_forwardings_dict[floating_ip.port_id] = pfwds
floating_ips_dict[floating_ip.port_id] = floating_ip

return network_ports, networks_dict, floating_ips_dict, port_forwardings_dict


def get_networks_from_port(connection, port, networks_dict={}, floating_ips_dict={}):
"""Gets associated network objects from a port object
:param connection: An OpenStack connection
:type connection: :class:`~openstack.connection.Connection`
:param port: A network port
:type port: :class:`~openstack.network.v2.port.Port`
:param networks_dict: A dictionary mapping network IDs to network objects
:param floating_ips_dict: A dictionary mapping port IDs to floating IPs
:returns: A tuple containing the parent network, trunk networks, trunk ports,
and the floating network associated with the given port
"""

parent_network = None
trunk_networks = []
trunk_ports = []

if port.network_id in networks_dict:
parent_network = networks_dict[port.network_id]
else:
parent_network = connection.network.get_network(network=port.network_id)

if port.trunk_details:
with concurrent.futures.ThreadPoolExecutor() as executor:
subport_futures = []
subport_infos = port.trunk_details['sub_ports']
for subport_info in subport_infos:
subport_futures.append(executor.submit(
connection.network.get_port,
port=subport_info['port_id']
))
for subport_future in subport_futures:
subport = subport_future.result()
if subport.network_id in networks_dict:
trunk_network = networks_dict[subport.network_id]
else:
trunk_network = connection.network.get_network(subport.network_id)
trunk_ports.append(subport)
trunk_networks.append(trunk_network)

floating_network_id = getattr(floating_ips_dict.get(port.id),
'floating_network_id', None)
if floating_network_id is None:
floating_network = None
elif networks_dict.get(floating_network_id):
floating_network = networks_dict[floating_network_id]
else:
floating_network = connection.network.get_network(floating_network_id)

return parent_network, trunk_networks, trunk_ports, floating_network
136 changes: 136 additions & 0 deletions esi/lib/nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@

# 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.

import concurrent.futures

from esi.lib import networks


def node_and_port_list(connection, filter_node=None):
"""Get lists baremetal nodes and ports
:param connection: An OpenStack connection
:type connection: :class:`~openstack.connection.Connection`
:param filter_node: The name or ID of a node
:returns: A tuple of lists of nodes and ports of the form:
(
[openstack.baremetal.v1.node.Node],
[openstack.baremetal.v1.port.Port]
)
"""

nodes = None
ports = None

if filter_node:
nodes = [connection.baremetal.find_node(name_or_id=filter_node,
ignore_missing=False)]
ports = connection.baremetal.ports(details=True, node_id=nodes[0].id)
else:
with concurrent.futures.ThreadPoolExecutor() as executor:
f1 = executor.submit(connection.baremetal.nodes)
f2 = executor.submit(connection.baremetal.ports, details=True)
nodes = list(f1.result())
ports = list(f2.result())

return nodes, ports


def network_list(connection, filter_node=None, filter_network=None):
"""List nodes and their network attributes
:param connection: An OpenStack connection
:type connection: :class:`~openstack.connection.Connection`
:param filter_node: the name or ID of a node
:param filter_network: The name or ID of a network
:returns: A list of dictionaries of the form:
{
'node': openstack.baremetal.v1.node.Node,
'network_info': [
{
'baremetal_port': openstack.baremetal.v1.port.Port,
'network_port': [openstack.network.v2.port.Port] or [],
'networks': {
'parent': openstack.network.v2.network.Network or None,
'trunk': [openstack.network.v2.network.Network] or [],
'floating': openstack.network.v2.network.Network or None,
},
'floating_ip': openstack.network.v2.floating_ip.FloatingIP or None,
'port_forwardings': [openstack.network.v2.port_forwarding.PortForwarding] or []
},
...
]
}
"""

with concurrent.futures.ThreadPoolExecutor() as executor:
f1 = executor.submit(node_and_port_list, connection, filter_node)
if filter_network:
f3 = executor.submit(connection.network.find_network,
name_or_id=filter_network,
ignore_missing=False)
filter_network = f3.result()
f2 = executor.submit(networks.network_and_port_list, connection, filter_network)
baremetal_nodes, baremetal_ports = f1.result()
network_ports, networks_dict, floating_ips_dict, port_forwardings_dict = f2.result()

data = []
for baremetal_node in baremetal_nodes:
network_info = []
node_ports = [bp for bp in baremetal_ports
if bp.node_id == baremetal_node.id]

for baremetal_port in node_ports:
network_port = None
network_port_id = baremetal_port.internal_info.get('tenant_vif_port_id', None)

if network_port_id:
network_port = next((np for np in network_ports
if np.id == network_port_id), None)

if network_port is not None and (not filter_network or filter_network.id == network_port.network_id):
parent_network, trunk_networks, trunk_ports, floating_network \
= networks.get_networks_from_port(connection,
network_port,
networks_dict,
floating_ips_dict)

network_info.append({
'baremetal_port': baremetal_port,
'network_ports': [network_port] + trunk_ports,
'networks': {
'parent': parent_network,
'trunk': trunk_networks,
'floating': floating_network
},
'floating_ip': floating_ips_dict.get(network_port.id, None),
'port_forwardings': port_forwardings_dict.get(network_port.id, []),
})
elif not filter_network:
network_info.append({
'baremetal_port': baremetal_port,
'network_ports': [],
'networks': {},
'floating_ip': None,
'port_forwardings': [],
})

if network_info != []:
data.append({
'node': baremetal_node,
'network_info': network_info
})

return data
32 changes: 32 additions & 0 deletions esi/tests/unit/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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.
#

import mock
import testtools


class TestCase(testtools.TestCase):
"""Base class for all unit tests"""

def setUp(self):
super(TestCase, self).setUp()


class TestCommand(TestCase):
"""Base class for all command unit tests"""

def setUp(self):
super(TestCommand, self).setUp()
self.connection = mock.Mock()
self.connection.baremetal = mock.Mock()
self.connection.network = mock.Mock()
Empty file added esi/tests/unit/lib/__init__.py
Empty file.
Loading

0 comments on commit 1b06247

Please sign in to comment.