Skip to content

Commit

Permalink
Migrate Node Network Attach Functionality Into esisdk
Browse files Browse the repository at this point in the history
Originally, this functionality existed in python-esiclient.
It is being moved into esisdk so both python-esiclient and
esi-ui can use it.
  • Loading branch information
ajamias committed Jul 19, 2024
1 parent e195735 commit cf32401
Show file tree
Hide file tree
Showing 2 changed files with 289 additions and 0 deletions.
85 changes: 85 additions & 0 deletions esi/lib/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import concurrent.futures

from openstack import exceptions

from esi.lib import networks


Expand Down Expand Up @@ -138,3 +140,86 @@ def network_list(connection, filter_node=None, filter_network=None):
})

return data


def network_attach(connection, node, filter_network=None, filter_port=None, filter_trunk=None, mac_address=None):
"""Attaches a node's bare metal port to a network port
:param connection: An OpenStack connection
:type connection: :class:`~openstack.connection.Connection`
:param node: The name or ID of a node
:param network: The name or ID of a network
:param port: The name or ID of a network port
:param trunk: The name or ID of a network trunk
:param mac_address: The mac address of the bare metal port to attach
:returns: a dictionary with the resulting node and network information
{
'node': openstack.baremetal.v1.node.Node,
'ports': [openstack.network.v2.port.Port]
'networks': [openstack.network.v2.network.Network]
}
"""

if (filter_network and filter_port) or (filter_network and filter_trunk) or (filter_port and filter_trunk):
raise exceptions.InvalidRequest('Specify only one of network, port or trunk')
if not filter_network and not filter_port and not filter_trunk:
raise exceptions.InvalidRequest('You must specify either network, port, or trunk')

if filter_network:
network = connection.network.find_network(filter_network, ignore_missing=False)
network_port = None
elif filter_port:
network_port = connection.network.find_port(filter_port, ignore_missing=False)
elif filter_trunk:
trunk = connection.network.find_trunk(filter_trunk, ignore_missing=False)
network_port = None

with concurrent.futures.ThreadPoolExecutor() as executor:
f1 = executor.submit(connection.baremetal.get_node, node)
f2 = executor.submit(connection.session.get_endpoint, service_type='baremetal', service_name='ironic', interface='public')
node = f1.result()
baremetal_endpoint = f2.result()

if mac_address:
baremetal_ports = list(connection.baremetal.ports(details=True, address=mac_address))
if len(baremetal_ports) == 0:
raise exceptions.ResourceFailure('MAC address {0} does not exist on node {1}'.format(mac_address, node.name))
else:
baremetal_ports = connection.baremetal.ports(details=True, node=node.id)
has_free_port = False
for bp in baremetal_ports:
if 'tenant_vif_port_id' not in bp.internal_info:
has_free_port = True
break

if not has_free_port:
raise exceptions.ResourceFailure('Node {0} has no free ports'.format(node.name))

if filter_network:
port_name = 'esi-{0}-{1}'.format(node.name, network.name)
network_ports = list(connection.network.ports(name=port_name, status='DOWN'))
if len(network_ports) > 0:
network_port = network_ports[0]
else:
network_port = connection.network.create_port(name=port_name, network_id=network.id, device_owner='baremetal:none')
elif filter_trunk:
network_port = connection.network.find_port(trunk.port_id, ignore_missing=False)

data = {'id': network_port.id}
if mac_address:
data['port_uuid'] = baremetal_ports[0].id
connection.session.post('{0}/v1/nodes/{1}/vifs'.format(baremetal_endpoint, node.name), json=data, headers={'X-OpenStack-Ironic-API-Version': '1.69'})
network_port = connection.network.find_port(network_port.id, ignore_missing=False)

network, trunk_networks, trunk_ports, _ \
= networks.get_networks_from_port(connection,
network_port,
networks_dict={network.id: network}
if 'network' in locals() else {})

return {
'node': node,
'ports': [network_port] + trunk_ports,
'networks': [network] + trunk_networks
}
204 changes: 204 additions & 0 deletions esi/tests/unit/lib/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,207 @@ def test_network_list_filter_node_network(self):
self.connection.network.ports.assert_called_once_with(network_id='network_uuid_2')
self.connection.network.port_forwardings.assert_called_once_with(floating_ip=self.floating_ip_pfw)
self.connection.network.get_port.assert_not_called()


class TestNetworkAttach(TestCase):

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

self.port1 = utils.create_mock_object({
"id": "port_uuid_1",
"node_uuid": "node_uuid_1",
"address": "aa:aa:aa:aa:aa:aa",
"internal_info": {'tenant_vif_port_id': 'neutron_port_uuid_1'}
})
self.port2 = utils.create_mock_object({
"id": "port_uuid_2",
"node_uuid": "node_uuid_1",
"address": "bb:bb:bb:bb:bb:bb",
"internal_info": {}
})
self.port3 = utils.create_mock_object({
"uuid": "port_uuid_3",
"node_uuid": "node_uuid_1",
"address": "cc:cc:cc:cc:cc:cc",
"internal_info": {}
})
self.node = utils.create_mock_object({
"id": "node_uuid_1",
"name": "node1",
"provision_state": "active"
})
self.node_available = utils.create_mock_object({
"uuid": "node_uuid_1",
"name": "node1",
"provision_state": "available"
})
self.node_manageable = utils.create_mock_object({
"uuid": "node_uuid_1",
"name": "node1",
"provision_state": "manageable",
"instance_info": {},
"driver_info": {'deploy_ramdisk': 'fake-image'},
})
self.node_manageable_instance_info = utils.create_mock_object({
"uuid": "node_uuid_1",
"name": "node1",
"provision_state": "manageable",
"instance_info": {'image_source': 'fake-image',
'capabilities': {}},
"driver_info": {'deploy_ramdisk': 'fake-image'},
})
self.network = utils.create_mock_object({
"id": "network_uuid",
"name": "test_network"
})
self.network2 = utils.create_mock_object({
"id": "network_uuid_2",
"name": "test_network_2"
})
self.neutron_port1 = utils.create_mock_object({
"id": "neutron_port_uuid_2",
"network_id": "network_uuid",
"name": "node1-port1",
"mac_address": "bb:bb:bb:bb:bb:bb",
"fixed_ips": [{"ip_address": "2.2.2.2"}],
"trunk_details": None
})
self.neutron_port2 = utils.create_mock_object({
"id": "neutron_port_uuid_3",
"network_id": "network_uuid_2",
"name": "node1-port2",
"mac_address": "cc:cc:cc:cc:cc:cc",
"fixed_ips": [{"ip_address": "3.3.3.3"}],
"trunk_details": {
'sub_ports': [
{'port_id': self.neutron_port1.id},
]
}
})
self.trunk = utils.create_mock_object({
"port_id": 'neutron_port_uuid_3',
"name": "test_trunk"
})

self.connection = mock.Mock()

self.connection.network.find_network.\
return_value = self.network
self.connection.session.get_endpoint.\
return_value = 'endpoint'
self.connection.network.create_port.\
return_value = self.neutron_port1
self.connection.network.ports.\
return_value = []
self.connection.network.find_trunk.\
return_value = self.trunk
self.connection.baremetal.get_node.\
return_value = self.node

def mock_baremetal_ports(details=False, node=None, address=None):
if node is None or node == 'node1' or node == 'node_uuid_1':
if address == "aa:aa:aa:aa:aa:aa":
return [self.port1]
if address == "bb:bb:bb:bb:bb:bb":
return [self.port2]
return [self.port1, self.port2]
return []
self.connection.baremetal.ports.side_effect = mock_baremetal_ports

def mock_find_port(port, ignore_missing=True):
if port == 'neutron_port_uuid_2' or port == 'node1-port1':
return self.neutron_port1
elif port == 'neutron_port_uuid_3' or port == 'node1-port2':
return self.neutron_port2
return None
self.connection.network.find_port.side_effect = mock_find_port

@mock.patch('esi.lib.networks.get_networks_from_port')
def test_network_attach_network(self, mock_gnfp):
mock_gnfp.return_value = (self.network, [], [], None)

actual = nodes.network_attach(self.connection,
'node1',
filter_network='test_network')

expected = {
'node': self.node,
'ports': [self.neutron_port1],
'networks': [self.network]
}

self.connection.network.find_network.assert_called_once_with('test_network', ignore_missing=False)
self.connection.baremetal.get_node.assert_called_once_with('node1')
self.connection.baremetal.ports.assert_called_once_with(details=True, node='node_uuid_1')
self.connection.network.ports.assert_called_once_with(name='esi-node1-test_network', status='DOWN')
self.connection.network.create_port.assert_called_once_with(name='esi-node1-test_network', network_id='network_uuid', device_owner='baremetal:none')
self.connection.session.post.assert_called_once_with('endpoint/v1/nodes/node1/vifs', json={'id': 'neutron_port_uuid_2'}, headers={'X-OpenStack-Ironic-API-Version': '1.69'})
self.connection.network.find_port.assert_called_once_with('neutron_port_uuid_2', ignore_missing=False)
self.assertEqual(expected, actual)

@mock.patch('esi.lib.networks.get_networks_from_port')
def test_network_attach_port(self, mock_gnfp):
mock_gnfp.return_value = (self.network, [], [], None)

actual = nodes.network_attach(self.connection,
'node1',
filter_port='node1-port1')

expected = {
'node': self.node,
'ports': [self.neutron_port1],
'networks': [self.network]
}

self.connection.network.find_port.assert_any_call('node1-port1', ignore_missing=False)
self.connection.baremetal.get_node.assert_called_once_with('node1')
self.connection.baremetal.ports.assert_called_once_with(details=True, node='node_uuid_1')
self.connection.session.post.assert_called_once_with('endpoint/v1/nodes/node1/vifs', json={'id': 'neutron_port_uuid_2'}, headers={'X-OpenStack-Ironic-API-Version': '1.69'})
self.connection.network.find_port.assert_any_call('neutron_port_uuid_2', ignore_missing=False)
self.assertEqual(expected, actual)

@mock.patch('esi.lib.networks.get_networks_from_port')
def test_network_attach_port_and_mac_address(self, mock_gnfp):
mock_gnfp.return_value = (self.network, [], [], None)

actual = nodes.network_attach(self.connection,
'node1',
filter_port='node1-port1',
mac_address='bb:bb:bb:bb:bb:bb')

expected = {
'node': self.node,
'ports': [self.neutron_port1],
'networks': [self.network]
}

self.connection.network.find_port.assert_any_call('node1-port1', ignore_missing=False)
self.connection.baremetal.get_node.assert_called_once_with('node1')
self.connection.session.get_endpoint.assert_called_once_with(service_type='baremetal', service_name='ironic', interface='public')
self.connection.baremetal.ports.assert_called_once_with(details=True, address='bb:bb:bb:bb:bb:bb')
self.connection.session.post.assert_called_once_with('endpoint/v1/nodes/node1/vifs', json={'id': 'neutron_port_uuid_2', 'port_uuid': 'port_uuid_2'}, headers={'X-OpenStack-Ironic-API-Version': '1.69'})
self.connection.network.find_port.assert_any_call('neutron_port_uuid_2', ignore_missing=False)
self.assertEqual(expected, actual)

@mock.patch('esi.lib.networks.get_networks_from_port')
def test_network_attach_trunk(self, mock_gnfp):
mock_gnfp.return_value = (self.network2, [self.network], [self.neutron_port1], None)

actual = nodes.network_attach(self.connection,
'node1',
filter_trunk='test_trunk')

expected = {
'node': self.node,
'ports': [self.neutron_port2, self.neutron_port1],
'networks': [self.network2, self.network]
}

self.connection.network.find_trunk.assert_called_once_with('test_trunk', ignore_missing=False)
self.connection.baremetal.get_node.assert_called_once_with('node1')
self.connection.baremetal.ports.assert_called_once_with(details=True, node='node_uuid_1')
self.connection.network.find_port.assert_any_call('neutron_port_uuid_3', ignore_missing=False)
self.connection.session.post.assert_called_once_with('endpoint/v1/nodes/node1/vifs', json={'id': 'neutron_port_uuid_3'}, headers={'X-OpenStack-Ironic-API-Version': '1.69'})
self.connection.network.find_port.assert_any_call('neutron_port_uuid_3', ignore_missing=False)
self.assertEqual(expected, actual)

0 comments on commit cf32401

Please sign in to comment.