baremetal: add support for VIF attach/detach API
Change-Id: Ifb311531e9750502a60afac0961e4919c4f8f1e5
This commit is contained in:
parent
ac8df03fd1
commit
d87624069f
@ -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
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
4
releasenotes/notes/baremetal-vif-122457118c722a9b.yaml
Normal file
4
releasenotes/notes/baremetal-vif-122457118c722a9b.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Implements VIF attach/detach API for bare metal nodes.
|
Loading…
x
Reference in New Issue
Block a user