Merge "Detach interface for server"

This commit is contained in:
Jenkins 2017-07-13 12:29:06 +00:00 committed by Gerrit Code Review
commit 4e1e190b62
14 changed files with 164 additions and 24 deletions

View File

@ -27,6 +27,12 @@ flavor_uuid_path:
in: path in: path
required: true required: true
type: string type: string
port_ident:
description: |
The UUID of a network port.
in: path
required: true
type: string
server_ident: server_ident:
description: | description: |
The UUID of the server. The UUID of the server.

View File

@ -126,3 +126,29 @@ Response
-------- --------
If successful, this method does not return content in the response body. If successful, this method does not return content in the response body.
Detach a network interface.
=================================
.. rest_method:: DELETE /v1/servers/{server_uuid}/networks/interfaces/{port_id}
Detach a network interface from a server.
Normal response codes: 204
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
itemNotFound(404), conflict(409)
Request
-------
.. rest_parameters:: parameters.yaml
- server_uuid: server_ident
- port_id: port_ident
Response
--------
If successful, this method does not return content in the response body.

View File

@ -201,7 +201,7 @@ class FloatingIPController(ServerControllerBase):
super(FloatingIPController, self).__init__(*args, **kwargs) super(FloatingIPController, self).__init__(*args, **kwargs)
self.network_api = network.API() self.network_api = network.API()
@policy.authorize_wsgi("mogan:server", "associate_floatingip", False) @policy.authorize_wsgi("mogan:server", "associate_floatingip")
@expose.expose(None, types.uuid, body=types.jsontype, @expose.expose(None, types.uuid, body=types.jsontype,
status_code=http_client.NO_CONTENT) status_code=http_client.NO_CONTENT)
def post(self, server_uuid, floatingip): def post(self, server_uuid, floatingip):
@ -354,7 +354,7 @@ class InterfaceController(ServerControllerBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InterfaceController, self).__init__(*args, **kwargs) super(InterfaceController, self).__init__(*args, **kwargs)
@policy.authorize_wsgi("mogan:server", "attach_interface", False) @policy.authorize_wsgi("mogan:server", "attach_interface")
@expose.expose(None, types.uuid, body=types.jsontype, @expose.expose(None, types.uuid, body=types.jsontype,
status_code=http_client.NO_CONTENT) status_code=http_client.NO_CONTENT)
def post(self, server_uuid, interface): def post(self, server_uuid, interface):
@ -385,6 +385,23 @@ class InterfaceController(ServerControllerBase):
raise wsme.exc.ClientSideError( raise wsme.exc.ClientSideError(
six.text_type(e), status_code=http_client.CONFLICT) six.text_type(e), status_code=http_client.CONFLICT)
@policy.authorize_wsgi("mogan:server", "detach_interface")
@expose.expose(None, types.uuid, types.uuid,
status_code=http_client.NO_CONTENT)
def delete(self, server_uuid, port_id):
"""Detach Interface
:param server_uuid: UUID of a server.
:param port_id: The Port ID within the request body.
"""
server = self._resource or self._get_resource(server_uuid)
server_nics = server.nics
if port_id not in [nic.port_id for nic in server_nics]:
raise exception.InterfaceNotFoundForServer(server=server_uuid)
pecan.request.engine_api.detach_interface(pecan.request.context,
server, port_id)
class ServerNetworks(base.APIBase): class ServerNetworks(base.APIBase):
"""API representation of the networks of a server.""" """API representation of the networks of a server."""

View File

@ -310,6 +310,18 @@ class InterfaceAttachFailed(Invalid):
"%(server_uuid)s") "%(server_uuid)s")
class InterfaceNotFoundForServer(NotFound):
_msg_fmt = _("Interface not found for server %(server)s.")
class InterfaceNotAttached(Invalid):
_msg_fmt = _("Interface is not attached.")
class InterfaceDetachFailed(Invalid):
_msg_fmt = _("Failed to detach network for %(server_uuid)s")
class FloatingIpNotFoundForAddress(NotFound): class FloatingIpNotFoundForAddress(NotFound):
_msg_fmt = _("Floating IP not found for address %(address)s.") _msg_fmt = _("Floating IP not found for address %(address)s.")

View File

@ -144,6 +144,9 @@ server_policies = [
policy.RuleDefault('mogan:node:get_all', policy.RuleDefault('mogan:node:get_all',
'rule:admin_api', 'rule:admin_api',
description='Retrieve all compute nodes'), description='Retrieve all compute nodes'),
policy.RuleDefault('mogan:server:detach_interface',
'rule:default',
description='Detach a network interface'),
] ]

View File

@ -516,3 +516,8 @@ class API(object):
@check_server_lock @check_server_lock
def attach_interface(self, context, server, net_id): def attach_interface(self, context, server, net_id):
self.engine_rpcapi.attach_interface(context, server, net_id) self.engine_rpcapi.attach_interface(context, server, net_id)
@check_server_lock
def detach_interface(self, context, server, port_id):
self.engine_rpcapi.detach_interface(context, server=server,
port_id=port_id)

View File

@ -84,8 +84,8 @@ class BaseEngineDriver(object):
""" """
raise NotImplementedError() raise NotImplementedError()
def unplug_vifs(self, context, server): def unplug_vif(self, context, server, port_id):
"""Unplug network interfaces. """Unplug network interface.
:param server: the server object. :param server: the server object.
""" """

View File

@ -261,27 +261,31 @@ class IronicDriver(base_driver.BaseEngineDriver):
def plug_vif(self, node_uuid, port_id): def plug_vif(self, node_uuid, port_id):
self.ironicclient.call("node.vif_attach", node_uuid, port_id) self.ironicclient.call("node.vif_attach", node_uuid, port_id)
def unplug_vifs(self, context, server): def unplug_vif(self, context, server, port_id):
LOG.debug("unplug: server_uuid=%(uuid)s vif=%(server_nics)s", LOG.debug("unplug: server_uuid=%(uuid)s vif=%(server_nics)s "
"port=%(port_id)s",
{'uuid': server.uuid, {'uuid': server.uuid,
'server_nics': str(server.nics)}) 'server_nics': str(server.nics),
patch = [{'op': 'remove', 'port_id': port_id})
'path': '/extra/vif_port_id'}] node = self._get_node(server.node_uuid)
self._unplug_vif(node, server, port_id)
ports = self.get_ports_from_node(server.node_uuid) def _unplug_vif(self, node, server, port_id):
for vif in server.nics:
for port in ports: if port_id == vif['port_id']:
try: try:
if 'vif_port_id' in port.extra: self.ironicclient.call("node.vif_detach", node.uuid,
self.ironicclient.call("port.update", port_id)
port.uuid, patch) except ironic.exc.BadRequest:
except client_e.BadRequest: LOG.debug(
pass "VIF %(vif)s isn't attached to Ironic node %(node)s",
{'vif': port_id, 'node': node.uuid})
def _cleanup_deploy(self, context, node, server): def _cleanup_deploy(self, context, node, server):
# NOTE(liusheng): here we may need to stop firewall if we have # NOTE(liusheng): here we may need to stop firewall if we have
# implemented in ironic like what Nova dose. # implemented in ironic like what Nova dose.
self.unplug_vifs(context, server) for vif in server.nics:
self.unplug_vif(context, server, vif['port_id'])
def spawn(self, context, server, configdrive_value): def spawn(self, context, server, configdrive_value):
"""Deploy a server. """Deploy a server.

View File

@ -508,8 +508,8 @@ class EngineManager(base_manager.BaseEngineManager):
LOG.error("Destroy networks for server %(uuid)s failed. " LOG.error("Destroy networks for server %(uuid)s failed. "
"Exception: %(exception)s", "Exception: %(exception)s",
{"uuid": server.uuid, "exception": e}) {"uuid": server.uuid, "exception": e})
for vif in server.nics:
self.driver.unplug_vifs(context, server) self.driver.unplug_vif(context, server, vif['port_id'])
self.driver.destroy(context, server) self.driver.destroy(context, server)
@wrap_server_fault @wrap_server_fault
@ -640,3 +640,24 @@ class EngineManager(base_manager.BaseEngineManager):
server.save() server.save()
except Exception as e: except Exception as e:
raise exception.InterfaceAttachFailed(message=six.text_type(e)) raise exception.InterfaceAttachFailed(message=six.text_type(e))
def detach_interface(self, context, server, port_id):
LOG.info('Detaching interface...', server=server)
try:
self.driver.unplug_vif(context, server, port_id)
except exception.MoganException as e:
LOG.warning("Detach interface failed, port_id=%(port_id)s,"
" reason: %(msg)s",
{'port_id': port_id, 'msg': six.text_type(e)})
raise exception.InterfaceDetachFailed(server_uuid=server.uuid)
else:
try:
self.network_api.delete_port(context, port_id, server.uuid)
except Exception as e:
raise exception.InterfaceDetachFailed(server_uuid=server.uuid)
for nic in server.nics:
if nic.port_id == port_id:
nic.delete(context)
LOG.info('Interface was successfully detached')

View File

@ -87,3 +87,8 @@ class EngineAPI(object):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host) cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
cctxt.call(context, 'attach_interface', cctxt.call(context, 'attach_interface',
server=server, net_id=net_id) server=server, net_id=net_id)
def detach_interface(self, context, server, port_id):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
cctxt.call(context, 'detach_interface', server=server,
port_id=port_id)

View File

@ -167,7 +167,7 @@ class BaremetalComputeAPIServersTest(base.BaseBaremetalComputeTest):
self.assertIn('subnet_id', fixed_ip) self.assertIn('subnet_id', fixed_ip)
self.assertIn('ip_address', fixed_ip) self.assertIn('ip_address', fixed_ip)
def test_server_attach_interface(self): def test_server_attach_detach_interface(self):
self._ensure_states_before_test() self._ensure_states_before_test()
nics_before = self.baremetal_compute_client.server_get_networks( nics_before = self.baremetal_compute_client.server_get_networks(
self.server_ids[0]) self.server_ids[0])
@ -185,6 +185,14 @@ class BaremetalComputeAPIServersTest(base.BaseBaremetalComputeTest):
fixed_ip = nic['fixed_ips'][0] fixed_ip = nic['fixed_ips'][0]
self.assertIn('subnet_id', fixed_ip) self.assertIn('subnet_id', fixed_ip)
self.assertIn('ip_address', fixed_ip) self.assertIn('ip_address', fixed_ip)
nics_before = self.baremetal_compute_client.server_get_networks(
self.server_ids[0])
port_id = nics_before[0]['port_id']
self.baremetal_compute_client.server_detach_interface(
self.server_ids[0], port_id=port_id)
nics_after = self.baremetal_compute_client.server_get_networks(
self.server_ids[0])
self.assertEqual(len(nics_before) - 1, len(nics_after))
def test_floatingip_association_disassociation(self): def test_floatingip_association_disassociation(self):
self._ensure_states_before_test() self._ensure_states_before_test()

View File

@ -225,6 +225,15 @@ class BaremetalComputeClient(rest_client.RestClient):
body = self.deserialize(body) body = self.deserialize(body)
return rest_client.ResponseBody(resp, body) return rest_client.ResponseBody(resp, body)
def server_detach_interface(self, server_id, port_id):
uri = '%s/servers/%s/networks/interfaces/%s' % (self.uri_prefix,
server_id, port_id)
resp, body = self.delete(uri)
self.expected_success(204, resp.status)
if body:
body = self.deserialize(body)
return rest_client.ResponseBody(resp, body)
class BaremetalNodeClient(rest_client.RestClient): class BaremetalNodeClient(rest_client.RestClient):
version = '1' version = '1'

View File

@ -372,3 +372,12 @@ class ComputeAPIUnitTest(base.DbTestCase):
azs = self.engine_api.list_availability_zones(self.context) azs = self.engine_api.list_availability_zones(self.context)
self.assertItemsEqual(['az1', 'az2'], azs['availability_zones']) self.assertItemsEqual(['az1', 'az2'], azs['availability_zones'])
@mock.patch.object(engine_rpcapi.EngineAPI, 'detach_interface')
def test_detach_interface(self, mock_detach_interface):
fake_server = db_utils.get_test_server(
user_id=self.user_id, project_id=self.project_id)
fake_server_obj = self._create_fake_server_obj(fake_server)
self.engine_api.detach_interface(self.context, fake_server_obj,
fake_server_obj['nics'][0]['port_id'])
self.assertTrue(mock_detach_interface.called)

View File

@ -53,7 +53,7 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin,
self.context, server_port_id, server.uuid) self.context, server_port_id, server.uuid)
@mock.patch.object(IronicDriver, 'destroy') @mock.patch.object(IronicDriver, 'destroy')
@mock.patch.object(IronicDriver, 'unplug_vifs') @mock.patch.object(IronicDriver, 'unplug_vif')
@mock.patch.object(manager.EngineManager, 'destroy_networks') @mock.patch.object(manager.EngineManager, 'destroy_networks')
def _test__delete_server(self, destroy_networks_mock, unplug_mock, def _test__delete_server(self, destroy_networks_mock, unplug_mock,
destroy_node_mock, state=None): destroy_node_mock, state=None):
@ -69,7 +69,7 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin,
self._stop_service() self._stop_service()
destroy_networks_mock.assert_called_once_with(self.context, server) destroy_networks_mock.assert_called_once_with(self.context, server)
unplug_mock.assert_called_once_with(self.context, server) self.assertEqual(unplug_mock.call_count, len(server.nics))
destroy_node_mock.assert_called_once_with(self.context, server) destroy_node_mock.assert_called_once_with(self.context, server)
def test__delete_server_cleaning(self): def test__delete_server_cleaning(self):
@ -148,6 +148,21 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin,
self.assertEqual('localhost', console['host']) self.assertEqual('localhost', console['host'])
self.assertIn('token', console) self.assertIn('token', console)
@mock.patch.object(network_api.API, 'delete_port')
@mock.patch.object(IronicDriver, 'unplug_vif')
def test_detach_interface(self, unplug_vif_mock, delete_port_mock):
fake_node = mock.MagicMock()
fake_node.provision_state = ironic_states.ACTIVE
server = obj_utils.create_test_server(
self.context, status=states.ACTIVE, node_uuid=None)
port_id = server['nics'][0]['port_id']
self._start_service()
self.service.detach_interface(self.context, server, port_id)
self._stop_service()
unplug_vif_mock.assert_called_once_with(self.context, server, port_id)
delete_port_mock.assert_called_once_with(self.context, port_id,
server.uuid)
def test_wrap_server_fault(self): def test_wrap_server_fault(self):
server = {"uuid": uuidutils.generate_uuid()} server = {"uuid": uuidutils.generate_uuid()}