baremetal: add support for VIF attach/detach API

Change-Id: Ifb311531e9750502a60afac0961e4919c4f8f1e5
This commit is contained in:
Dmitry Tantsur 2018-08-13 11:50:57 +02:00
parent ac8df03fd1
commit d87624069f
10 changed files with 386 additions and 6 deletions

View File

@ -65,6 +65,14 @@ Chassis Operations
.. automethod:: openstack.baremetal.v1._proxy.Proxy.find_chassis
.. automethod:: openstack.baremetal.v1._proxy.Proxy.chassis
VIF Operations
^^^^^^^^^^^^^^
.. autoclass:: openstack.baremetal.v1._proxy.Proxy
.. automethod:: openstack.baremetal.v1._proxy.Proxy.attach_vif_to_node
.. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node
.. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs
Deprecated Methods
^^^^^^^^^^^^^^^^^^

View File

@ -52,3 +52,6 @@ STATE_VERSIONS = {
'manageable': '1.4',
}
"""API versions when certain states were introduced."""
VIF_VERSION = '1.28'
"""API version in which the VIF operations were introduced."""

View File

@ -702,3 +702,58 @@ class Proxy(proxy.Proxy):
"""
return self._delete(_portgroup.PortGroup, port_group,
ignore_missing=ignore_missing)
def attach_vif_to_node(self, node, vif_id):
"""Attach a VIF to the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID. A VIF can only be attached to one node
at a time.
:param node: The value can be either the name or ID of a node or
a :class:`~openstack.baremetal.v1.node.Node` instance.
:param string vif_id: Backend-specific VIF ID.
:return: ``None``
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
res = self._get_resource(_node.Node, node)
res.attach_vif(self, vif_id)
def detach_vif_from_node(self, node, vif_id, ignore_missing=True):
"""Detach a VIF from the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID.
:param node: The value can be either the name or ID of a node or
a :class:`~openstack.baremetal.v1.node.Node` instance.
:param string vif_id: Backend-specific VIF ID.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the VIF does not exist. Otherwise, ``False``
is returned.
:return: ``True`` if the VIF was detached, otherwise ``False``.
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
res = self._get_resource(_node.Node, node)
return res.detach_vif(self, vif_id, ignore_missing=ignore_missing)
def list_node_vifs(self, node):
"""List IDs of VIFs attached to the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID.
:param node: The value can be either the name or ID of a node or
a :class:`~openstack.baremetal.v1.node.Node` instance.
:return: List of VIF IDs as strings.
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
res = self._get_resource(_node.Node, node)
return res.list_vifs(self)

View File

@ -41,8 +41,8 @@ class Node(resource.Resource):
is_maintenance='maintenance',
)
# Full port groups support introduced in 1.24
_max_microversion = '1.24'
# VIF attach/detach support introduced in 1.28.
_max_microversion = '1.28'
# Properties
#: The UUID of the chassis associated wit this node. Can be empty or None.
@ -341,6 +341,107 @@ class Node(resource.Resource):
"the last error is %(error)s" %
{'node': self.id, 'error': self.last_error})
def attach_vif(self, session, vif_id):
"""Attach a VIF to the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID. A VIF can only be attached to one node
at a time.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param string vif_id: Backend-specific VIF ID.
:return: ``None``
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
session = self._get_session(session)
version = self._assert_microversion_for(
session, 'commit', _common.VIF_VERSION,
error_message=("Cannot use VIF attachment API"))
request = self._prepare_request(requires_id=True)
request.url = utils.urljoin(request.url, 'vifs')
body = {'id': vif_id}
response = session.post(
request.url, json=body,
headers=request.headers, microversion=version,
# NOTE(dtantsur): do not retry CONFLICT, it's a valid status code
# in this API when the VIF is already attached to another node.
retriable_status_codes=[503])
msg = ("Failed to attach VIF {vif} to bare metal node {node}"
.format(node=self.id, vif=vif_id))
exceptions.raise_from_response(response, error_message=msg)
def detach_vif(self, session, vif_id, ignore_missing=True):
"""Detach a VIF from the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param string vif_id: Backend-specific VIF ID.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the VIF does not exist. Otherwise, ``False``
is returned.
:return: ``True`` if the VIF was detached, otherwise ``False``.
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
session = self._get_session(session)
version = self._assert_microversion_for(
session, 'commit', _common.VIF_VERSION,
error_message=("Cannot use VIF attachment API"))
request = self._prepare_request(requires_id=True)
request.url = utils.urljoin(request.url, 'vifs', vif_id)
response = session.delete(
request.url, headers=request.headers, microversion=version,
retriable_status_codes=_common.RETRIABLE_STATUS_CODES)
if ignore_missing and response.status_code == 400:
_logger.debug('VIF %(vif)s was already removed from node %(node)s',
{'vif': vif_id, 'node': self.id})
return False
msg = ("Failed to detach VIF {vif} from bare metal node {node}"
.format(node=self.id, vif=vif_id))
exceptions.raise_from_response(response, error_message=msg)
return True
def list_vifs(self, session):
"""List IDs of VIFs attached to the node.
The exact form of the VIF ID depends on the network interface used by
the node. In the most common case it is a Network service port
(NOT a Bare Metal port) ID.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:return: List of VIF IDs as strings.
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
does not support the VIF API.
"""
session = self._get_session(session)
version = self._assert_microversion_for(
session, 'fetch', _common.VIF_VERSION,
error_message=("Cannot use VIF attachment API"))
request = self._prepare_request(requires_id=True)
request.url = utils.urljoin(request.url, 'vifs')
response = session.get(
request.url, headers=request.headers, microversion=version)
msg = ("Failed to list VIFs attached to bare metal node {node}"
.format(node=self.id))
exceptions.raise_from_response(response, error_message=msg)
return [vif['id'] for vif in response.json()['vifs']]
class NodeDetail(Node):

View File

@ -9821,6 +9821,40 @@ class OpenStackCloud(_normalize.Normalizer):
changes=change_list
)
def attach_port_to_machine(self, name_or_id, port_name_or_id):
"""Attach a virtual port to the bare metal machine.
:param string name_or_id: A machine name or UUID.
:param string port_name_or_id: A port name or UUID.
Note that this is a Network service port, not a bare metal NIC.
:return: Nothing.
"""
machine = self.get_machine(name_or_id)
port = self.get_port(port_name_or_id)
self.baremetal.attach_vif_to_node(machine, port['id'])
def detach_port_from_machine(self, name_or_id, port_name_or_id):
"""Detach a virtual port from the bare metal machine.
:param string name_or_id: A machine name or UUID.
:param string port_name_or_id: A port name or UUID.
Note that this is a Network service port, not a bare metal NIC.
:return: Nothing.
"""
machine = self.get_machine(name_or_id)
port = self.get_port(port_name_or_id)
self.baremetal.detach_vif_from_node(machine, port['id'])
def list_ports_attached_to_machine(self, name_or_id):
"""List virtual ports attached to the bare metal machine.
:param string name_or_id: A machine name or UUID.
:returns: List of ``munch.Munch`` representing the ports.
"""
machine = self.get_machine(name_or_id)
vif_ids = self.baremetal.list_node_vifs(machine)
return [self.get_port(vif) for vif in vif_ids]
def validate_node(self, uuid):
# TODO(TheJulia): There are soooooo many other interfaces
# that we can support validating, while these are essential,

View File

@ -68,3 +68,34 @@ class TestBareMetalNode(base.BaseBaremetalTest):
ignore_missing=False)
self.assertIsNone(self.conn.baremetal.find_node(uuid))
self.assertIsNone(self.conn.baremetal.delete_node(uuid))
class TestBareMetalVif(base.BaseBaremetalTest):
min_microversion = '1.28'
def setUp(self):
super(TestBareMetalVif, self).setUp()
self.node = self.create_node(network_interface='noop')
self.vif_id = "200712fc-fdfb-47da-89a6-2d19f76c7618"
def test_node_vif_attach_detach(self):
self.conn.baremetal.attach_vif_to_node(self.node, self.vif_id)
# NOTE(dtantsur): The noop networking driver is completely noop - the
# VIF list does not return anything of value.
self.conn.baremetal.list_node_vifs(self.node)
res = self.conn.baremetal.detach_vif_from_node(self.node, self.vif_id,
ignore_missing=False)
self.assertTrue(res)
def test_node_vif_negative(self):
uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971"
self.assertRaises(exceptions.NotFoundException,
self.conn.baremetal.attach_vif_to_node,
uuid, self.vif_id)
self.assertRaises(exceptions.NotFoundException,
self.conn.baremetal.list_node_vifs,
uuid)
self.assertRaises(exceptions.NotFoundException,
self.conn.baremetal.detach_vif_from_node,
uuid, self.vif_id, ignore_missing=False)

View File

@ -13,6 +13,7 @@
from keystoneauth1 import adapter
import mock
from openstack.baremetal.v1 import _common
from openstack.baremetal.v1 import node
from openstack import exceptions
from openstack.tests.unit import base
@ -371,3 +372,63 @@ class TestNodeCreate(base.TestCase):
headers=mock.ANY, microversion=self.session.default_microversion)
mock_prov.assert_called_once_with(self.node, self.session, 'manage',
wait=True)
@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
@mock.patch.object(node.Node, '_get_session', lambda self, x: x)
class TestNodeVif(base.TestCase):
def setUp(self):
super(TestNodeVif, self).setUp()
self.session = mock.Mock(spec=adapter.Adapter)
self.session.default_microversion = '1.28'
self.node = node.Node(id='c29db401-b6a7-4530-af8e-20a720dee946',
driver=FAKE['driver'])
self.vif_id = '714bdf6d-2386-4b5e-bd0d-bc036f04b1ef'
def test_attach_vif(self):
self.assertIsNone(self.node.attach_vif(self.session, self.vif_id))
self.session.post.assert_called_once_with(
'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id},
headers=mock.ANY, microversion='1.28',
retriable_status_codes=[503])
def test_detach_vif_existing(self):
self.assertTrue(self.node.detach_vif(self.session, self.vif_id))
self.session.delete.assert_called_once_with(
'nodes/%s/vifs/%s' % (self.node.id, self.vif_id),
headers=mock.ANY, microversion='1.28',
retriable_status_codes=_common.RETRIABLE_STATUS_CODES)
def test_detach_vif_missing(self):
self.session.delete.return_value.status_code = 400
self.assertFalse(self.node.detach_vif(self.session, self.vif_id))
self.session.delete.assert_called_once_with(
'nodes/%s/vifs/%s' % (self.node.id, self.vif_id),
headers=mock.ANY, microversion='1.28',
retriable_status_codes=_common.RETRIABLE_STATUS_CODES)
def test_list_vifs(self):
self.session.get.return_value.json.return_value = {
'vifs': [
{'id': '1234'},
{'id': '5678'},
]
}
res = self.node.list_vifs(self.session)
self.assertEqual(['1234', '5678'], res)
self.session.get.assert_called_once_with(
'nodes/%s/vifs' % self.node.id,
headers=mock.ANY, microversion='1.28')
def test_incompatible_microversion(self):
self.session.default_microversion = '1.1'
self.assertRaises(exceptions.NotSupported,
self.node.attach_vif,
self.session, self.vif_id)
self.assertRaises(exceptions.NotSupported,
self.node.detach_vif,
self.session, self.vif_id)
self.assertRaises(exceptions.NotSupported,
self.node.list_vifs,
self.session)

View File

@ -689,7 +689,8 @@ class IronicTestCase(TestCase):
self.uuid = str(uuid.uuid4())
self.name = self.getUniqueString('name')
def get_mock_url(self, resource=None, append=None, qs_elements=None):
return super(IronicTestCase, self).get_mock_url(
service_type='baremetal', interface='public', resource=resource,
append=append, base_url_append='v1', qs_elements=qs_elements)
def get_mock_url(self, **kwargs):
kwargs.setdefault('service_type', 'baremetal')
kwargs.setdefault('interface', 'public')
kwargs.setdefault('base_url_append', 'v1')
return super(IronicTestCase, self).get_mock_url(**kwargs)

View File

@ -1609,6 +1609,88 @@ class TestBaremetalNode(base.IronicTestCase):
self.assert_calls()
def test_attach_port_to_machine(self):
vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec'
self.register_uris([
dict(
method='GET',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid']]),
json=self.fake_baremetal_node),
dict(
method='GET',
uri=self.get_mock_url(
service_type='network',
resource='ports.json',
base_url_append='v2.0'),
json={'ports': [{'id': vif_id}]}),
dict(
method='POST',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid'], 'vifs'])),
])
self.cloud.attach_port_to_machine(self.fake_baremetal_node['uuid'],
vif_id)
self.assert_calls()
def test_detach_port_from_machine(self):
vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec'
self.register_uris([
dict(
method='GET',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid']]),
json=self.fake_baremetal_node),
dict(
method='GET',
uri=self.get_mock_url(
service_type='network',
resource='ports.json',
base_url_append='v2.0'),
json={'ports': [{'id': vif_id}]}),
dict(
method='DELETE',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid'], 'vifs',
vif_id])),
])
self.cloud.detach_port_from_machine(self.fake_baremetal_node['uuid'],
vif_id)
self.assert_calls()
def test_list_ports_attached_to_machine(self):
vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec'
fake_port = {'id': vif_id, 'name': 'test'}
self.register_uris([
dict(
method='GET',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid']]),
json=self.fake_baremetal_node),
dict(
method='GET',
uri=self.get_mock_url(
resource='nodes',
append=[self.fake_baremetal_node['uuid'], 'vifs']),
json={'vifs': [{'id': vif_id}]}),
dict(
method='GET',
uri=self.get_mock_url(
service_type='network',
resource='ports.json',
base_url_append='v2.0'),
json={'ports': [fake_port]}),
])
res = self.cloud.list_ports_attached_to_machine(
self.fake_baremetal_node['uuid'])
self.assert_calls()
self.assertEqual([fake_port], res)
class TestUpdateMachinePatch(base.IronicTestCase):
# NOTE(TheJulia): As appears, and mordred describes,

View File

@ -0,0 +1,4 @@
---
features:
- |
Implements VIF attach/detach API for bare metal nodes.