Add support for network adapter hotplug.

This patch makes it possible to add/del instance
    interface other than booting time.
    Implement bp:network-adapter-hotplug

Originally from change Ibee003a9ec6cc9b3fd275417caccd0c67f6c871f

Co-authored-by: Yaguang Tang <heut2008@gmail.com>
Co-authored-by: Édouard Thuleau <edouard.thuleau@orange.com>

Change-Id: I4f8f677af58afcb928379e5cf859388d1da45d51
This commit is contained in:
Dan Smith 2013-02-12 15:45:24 -05:00
parent 1b9b66ba21
commit a9add7d35e
45 changed files with 1233 additions and 17 deletions

View File

@ -96,6 +96,14 @@
"namespace": "http://docs.openstack.org/compute/ext/aggregates/api/v1.1",
"updated": "2012-01-12T00:00:00+00:00"
},
{
"alias": "os-attach-interfaces",
"description": "Attach interface support.",
"links": [],
"name": "AttachInterfaces",
"namespace": "http://docs.openstack.org/compute/ext/interfaces/api/v1.1",
"updated": "2012-07-22T00:00:00+00:00"
},
{
"alias": "os-availability-zone",
"description": "1. Add availability_zone to the Create Server v1.1 API.\n 2. Add availability zones describing.\n ",
@ -194,11 +202,11 @@
},
{
"alias": "os-evacuate",
"description": "Enables server evacuation",
"description": "Enables server evacuation.",
"links": [],
"name": "Evacuate",
"namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
"updated": "2012-12-05T00:00:00+00:00"
"updated": "2013-01-06T00:00:00+00:00"
},
{
"alias": "os-fixed-ips",

View File

@ -40,6 +40,9 @@
<extension alias="os-aggregates" updated="2012-01-12T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/aggregates/api/v1.1" name="Aggregates">
<description>Admin-only aggregate administration.</description>
</extension>
<extension alias="os-attach-interfaces" updated="2012-07-22T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/interfaces/api/v1.1" name="AttachInterfaces">
<description>Attach interface support.</description>
</extension>
<extension alias="os-availability-zone" updated="2012-12-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone">
<description>1. Add availability_zone to the Create Server v1.1 API.
2. Add availability zones describing.
@ -88,8 +91,8 @@
<extension alias="os-deferred-delete" updated="2011-09-01T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1" name="DeferredDelete">
<description>Instance deferred delete.</description>
</extension>
<extension alias="os-evacuate" updated="2012-12-05T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/evacuate/api/v2" name="Evacuate">
<description>Enables server evacuation</description>
<extension alias="os-evacuate" updated="2013-01-06T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/evacuate/api/v2" name="Evacuate">
<description>Enables server evacuation.</description>
</extension>
<extension alias="os-fixed-ips" updated="2012-10-18T13:25:27-06:00" namespace="http://docs.openstack.org/compute/ext/fixed_ips/api/v2" name="FixedIPs">
<description>Fixed IPs support.</description>

View File

@ -0,0 +1,5 @@
{
"interfaceAttachment": {
"port_id": "ce531f90-199f-48c0-816c-13e38010b442"
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachment>
<port_id>ce531f90-199f-48c0-816c-13e38010b442</port_id>
</interfaceAttachment>

View File

@ -0,0 +1,12 @@
{
"interfaceAttachment": {
"fixed_ips": [{
"ip_address": "192.168.1.1",
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}],
"mac_addr": "fa:16:3e:4c:2c:30",
"net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
"port_state": "ACTIVE"
}
}

View File

@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<interfaceAttachment>
<net_id>3cb9bc59-5699-4588-a4b1-b87f96708bc6</net_id>
<port_id>ce531f90-199f-48c0-816c-13e38010b442</port_id>
<fixed_ips>
<fixed_ip>
<subnet_id>f8a6e8f8-c2ec-497c-9f23-da9616de54ef</subnet_id>
<ip_address>192.168.1.3</ip_address>
</fixed_ip>
</fixed_ips>
<port_state>ACTIVE</port_state>
<mac_addr>fa:16:3e:4c:2c:30</mac_addr>
</interfaceAttachment>

View File

@ -0,0 +1,16 @@
{
"interfaceAttachments": [
{
"port_state": "ACTIVE",
"fixed_ips": [
{
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef",
"ip_address": "192.168.1.3"
}
],
"net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
"mac_addr": "fa:16:3e:4c:2c:30"
}
]
}

View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<interfaceAttachments>
<interfaceAttachment>
<port_state>ACTIVE</port_state>
<fixed_ips>
<fixed_ip>
<subnet_id>f8a6e8f8-c2ec-497c-9f23-da9616de54ef</subnet_id>
<ip_address>192.168.1.3</ip_address>
</fixed_ip>
</fixed_ips>
<port_id>ce531f90-199f-48c0-816c-13e38010b442</port_id>
<net_id>3cb9bc59-5699-4588-a4b1-b87f96708bc6</net_id>
<mac_addr>fa:16:3e:4c:2c:30</mac_addr>
</interfaceAttachment>
</interfaceAttachments>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachments>
<interfaceAttachment>
<item>
<port_state>ACTIVE</port_state>
<fixed_ips>
<fixed_ip>
<subnet_id>f8a6e8f8-c2ec-497c-9f23-da9616de54ef</subnet_id>
<ip_address>192.168.1.3</ip_address>
</fixed_ip>
</fixed_ips>
<port_id>ce531f90-199f-48c0-816c-13e38010b442</port_id>
<net_id>3cb9bc59-5699-4588-a4b1-b87f96708bc6</net_id>
<mac_addr>fa:16:3e:4c:2c:30</mac_addr>
</item>
</interfaceAttachment>
</interfaceAttachments>

View File

@ -0,0 +1,14 @@
{
"interfaceAttachment": {
"port_state": "ACTIVE",
"fixed_ips": [
{
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef",
"ip_address": "192.168.1.3"
}
],
"net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
"mac_addr": "fa:16:3e:4c:2c:30"
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachment>
<port_state>ACTIVE</port_state>
<fixed_ips>
<fixed_ip>
<subnet_id>b6e47749-6bf0-4d6e-ae4b-ba6b5e238510</subnet_id>
<ip_address>192.168.123.131</ip_address>
</fixed_ip>
</fixed_ips>
<port_id>89e64f2e-86bd-4c19-9155-4548b36fdcb2</port_id>
<net_id>a9efd207-2c1a-4cdd-a296-d3c7c3211302</net_id>
<mac_addr>fa:16:3e:a4:1c:12</mac_addr>
</interfaceAttachment>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "N4Lxd6cMUXmE",
"id": "4e44ac84-f3ed-4219-aa2e-b3d1477f0ac3",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/4e44ac84-f3ed-4219-aa2e-b3d1477f0ac3",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/4e44ac84-f3ed-4219-aa2e-b3d1477f0ac3",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="71f1047f-f5db-42f9-b43f-85767bcafda6" adminPass="XVCtnj5P2MnJ">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/71f1047f-f5db-42f9-b43f-85767bcafda6" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/71f1047f-f5db-42f9-b43f-85767bcafda6" rel="bookmark"/>
</server>

View File

@ -29,6 +29,7 @@
"compute_extension:admin_actions:migrate": "rule:admin_api",
"compute_extension:aggregates": "rule:admin_api",
"compute_extension:agents": "rule:admin_api",
"compute_extension:attach_interfaces": "",
"compute_extension:baremetal_nodes": "rule:admin_api",
"compute_extension:cells": "rule:admin_api",
"compute_extension:certificates": "",

View File

@ -0,0 +1,192 @@
# Copyright 2012 SINA Inc.
# All Rights Reserved.
#
# 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.
"""The instance interfaces extension."""
import webob
from webob import exc
from nova.api.openstack import extensions
from nova import compute
from nova import exception
from nova import network
from nova.openstack.common import log as logging
LOG = logging.getLogger(__name__)
authorize = extensions.extension_authorizer('compute', 'attach_interfaces')
def _translate_interface_attachment_view(port_info):
"""Maps keys for interface attachment details view."""
return {
'net_id': port_info['network_id'],
'port_id': port_info['id'],
'mac_addr': port_info['mac_address'],
'port_state': port_info['status'],
'fixed_ips': port_info.get('fixed_ips', None),
}
class InterfaceAttachmentController(object):
"""The interface attachment API controller for the OpenStack API."""
def __init__(self):
self.compute_api = compute.API()
self.network_api = network.API()
super(InterfaceAttachmentController, self).__init__()
def index(self, req, server_id):
"""Returns the list of interface attachments for a given instance."""
return self._items(req, server_id,
entity_maker=_translate_interface_attachment_view)
def show(self, req, server_id, id):
"""Return data about the given interface attachment."""
context = req.environ['nova.context']
authorize(context)
port_id = id
try:
instance = self.compute_api.get(context, server_id)
except exception.NotFound:
raise exc.HTTPNotFound()
try:
port_info = self.network_api.show_port(context, port_id)
except exception.NotFound:
raise exc.HTTPNotFound()
if port_info['port']['device_id'] != server_id:
raise exc.HTTPNotFound()
return {'interfaceAttachment': _translate_interface_attachment_view(
port_info['port'])}
def create(self, req, server_id, body):
"""Attach an interface to an instance."""
context = req.environ['nova.context']
authorize(context)
network_id = None
port_id = None
req_ip = None
if body:
attachment = body['interfaceAttachment']
network_id = attachment.get('net_id', None)
port_id = attachment.get('port_id', None)
try:
req_ip = attachment['fixed_ips'][0]['ip_address']
except Exception:
pass
if network_id and port_id:
raise exc.HTTPBadRequest()
if req_ip and not network_id:
raise exc.HTTPBadRequest()
try:
instance = self.compute_api.get(context, server_id)
LOG.audit(_("Attach interface"), instance=instance)
network_info = self.compute_api.attach_interface(context,
instance, network_id, port_id, req_ip)
except exception.NotFound, e:
LOG.exception(e)
raise exc.HTTPNotFound()
except NotImplementedError:
msg = _("Network driver does not support this function.")
raise webob.exc.HTTPNotImplemented(explanation=msg)
except exception.InterfaceAttachFailed, e:
LOG.exception(e)
msg = _("Failed to attach interface")
raise webob.exc.HTTPInternalServerError(explanation=msg)
network, mapping = network_info
return self.show(req, server_id, mapping['vif_uuid'])
def update(self, req, server_id, id, body):
"""Update a interface attachment. We don't currently support this."""
msg = _("Attachments update is not supported")
raise exc.HTTPNotImplemented(explanation=msg)
def delete(self, req, server_id, id):
"""Detach an interface from an instance."""
context = req.environ['nova.context']
authorize(context)
port_id = id
try:
instance = self.compute_api.get(context, server_id)
LOG.audit(_("Detach interface %s"), port_id, instance=instance)
except exception.NotFound:
raise exc.HTTPNotFound()
try:
self.compute_api.detach_interface(context,
instance, port_id=port_id)
except exception.PortNotFound:
raise exc.HTTPNotFound
except NotImplementedError:
msg = _("Network driver does not support this function.")
raise webob.exc.HTTPNotImplemented(explanation=msg)
return webob.Response(status_int=202)
def _items(self, req, server_id, entity_maker):
"""Returns a list of attachments, transformed through entity_maker."""
context = req.environ['nova.context']
authorize(context)
try:
instance = self.compute_api.get(context, server_id)
except exception.NotFound:
raise exc.HTTPNotFound()
results = []
search_opts = {'device_id': instance['uuid']}
try:
data = self.network_api.list_ports(context, **search_opts)
except exception.NotFound:
raise exc.HTTPNotFound()
except NotImplementedError:
msg = _("Network driver does not support this function.")
raise webob.exc.HTTPNotImplemented(explanation=msg)
ports = data.get('ports', [])
results = [entity_maker(port) for port in ports]
return {'interfaceAttachments': results}
class Attach_interfaces(extensions.ExtensionDescriptor):
"""Attach interface support."""
name = "AttachInterfaces"
alias = "os-attach-interfaces"
namespace = "http://docs.openstack.org/compute/ext/interfaces/api/v1.1"
updated = "2012-07-22T00:00:00+00:00"
def get_resources(self):
resources = []
res = extensions.ResourceExtension('os-interface',
InterfaceAttachmentController(),
parent=dict(
member_name='server',
collection_name='servers'))
resources.append(res)
return resources

View File

@ -2284,6 +2284,20 @@ class API(base.Base):
raise exception.VolumeUnattached(volume_id=volume_id)
self._detach_volume(context, instance, volume_id)
@wrap_check_policy
def attach_interface(self, context, instance, network_id, port_id,
requested_ip):
"""Use hotplug to add an network adapter to an instance."""
return self.compute_rpcapi.attach_interface(context,
instance=instance, network_id=network_id, port_id=port_id,
requested_ip=requested_ip)
@wrap_check_policy
def detach_interface(self, context, instance, port_id):
"""Detach an network adapter from an instance."""
self.compute_rpcapi.detach_interface(context, instance=instance,
port_id=port_id)
@wrap_check_policy
def get_instance_metadata(self, context, instance):
"""Get all metadata associated with an instance."""

View File

@ -315,7 +315,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.SchedulerDependentManager):
"""Manages the running instances from creation to destruction."""
RPC_API_VERSION = '2.24'
RPC_API_VERSION = '2.25'
def __init__(self, compute_driver=None, *args, **kwargs):
"""Load configuration options and connect to the hypervisor."""
@ -2691,6 +2691,39 @@ class ComputeManager(manager.SchedulerDependentManager):
except exception.NotFound:
pass
def attach_interface(self, context, instance, network_id, port_id,
requested_ip=None):
"""Use hotplug to add an network adapter to an instance."""
network_info = self.network_api.allocate_port_for_instance(
context, instance, port_id, network_id, requested_ip,
self.conductor_api)
image_meta = _get_image_meta(context, instance['image_ref'])
legacy_net_info = self._legacy_nw_info(network_info)
for (network, mapping) in legacy_net_info:
if mapping['vif_uuid'] == port_id:
self.driver.attach_interface(instance, image_meta,
[(network, mapping)])
return (network, mapping)
def detach_interface(self, context, instance, port_id):
"""Detach an network adapter from an instance."""
network_info = self.network_api.get_instance_nw_info(
context.elevated(), instance, conductor_api=self.conductor_api)
legacy_nwinfo = self._legacy_nw_info(network_info)
condemned = None
for (network, mapping) in legacy_nwinfo:
if mapping['vif_uuid'] == port_id:
condemned = (network, mapping)
break
if condemned is None:
raise exception.PortNotFound(_("Port %(port_id)s is not "
"attached") % locals())
self.network_api.deallocate_port_for_instance(context, instance,
port_id,
self.conductor_api)
self.driver.detach_interface(instance, [condemned])
def _get_compute_info(self, context, host):
compute_node_ref = self.conductor_api.service_get_by_compute_host(
context, host)

View File

@ -159,6 +159,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
rebuild_instance()
2.23 - Remove network_info from reboot_instance
2.24 - Added get_spice_console method
2.25 - Add attach_interface() and detach_interface()
'''
#
@ -200,6 +201,15 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
instance=instance_p, network_id=network_id),
topic=_compute_topic(self.topic, ctxt, None, instance))
def attach_interface(self, ctxt, instance, network_id, port_id,
requested_ip):
instance_p = jsonutils.to_primitive(instance)
return self.call(ctxt, self.make_msg('attach_interface',
instance=instance_p, network_id=network_id,
port_id=port_id, requested_ip=requested_ip),
topic=_compute_topic(self.topic, ctxt, None, instance),
version='2.25')
def attach_volume(self, ctxt, instance, volume_id, mountpoint):
instance_p = jsonutils.to_primitive(instance)
self.cast(ctxt, self.make_msg('attach_volume',
@ -243,6 +253,13 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
topic=_compute_topic(self.topic, ctxt, host, instance),
version='2.7')
def detach_interface(self, ctxt, instance, port_id):
instance_p = jsonutils.to_primitive(instance)
self.cast(ctxt, self.make_msg('detach_interface',
instance=instance_p, port_id=port_id),
topic=_compute_topic(self.topic, ctxt, None, instance),
version='2.25')
def detach_volume(self, ctxt, instance, volume_id):
instance_p = jsonutils.to_primitive(instance)
self.cast(ctxt, self.make_msg('detach_volume',

View File

@ -496,6 +496,10 @@ class NetworkNotFound(NotFound):
message = _("Network %(network_id)s could not be found.")
class PortNotFound(NotFound):
message = _("Port id %(port_id)s could not be found.")
class NetworkNotFoundForBridge(NetworkNotFound):
message = _("Network could not be found for bridge %(bridge)s")
@ -529,10 +533,6 @@ class PortInUse(NovaException):
message = _("Port %(port_id)s is still in use.")
class PortNotFound(NotFound):
message = _("Port %(port_id)s could not be found.")
class PortNotUsable(NovaException):
message = _("Port %(port_id)s not usable for instance %(instance)s.")
@ -1076,6 +1076,14 @@ class ConfigDriveUnknownFormat(NovaException):
"iso9660 or vfat.")
class InterfaceAttachFailed(Invalid):
message = _("Failed to attach network adapter device to %(instance)s")
class InterfaceDetachFailed(Invalid):
message = _("Failed to detach network adapter device from %(instance)s")
class InstanceUserDataTooLarge(NovaException):
message = _("User data too large. User data must be no larger than "
"%(maxsize)s bytes once base64 encoded. Your data is "

View File

@ -282,6 +282,25 @@ class API(base.Base):
args['host'] = instance['host']
self.network_rpcapi.deallocate_for_instance(context, **args)
# NOTE(danms): Here for quantum compatibility
def allocate_port_for_instance(self, context, instance, port_id,
network_id=None, requested_ip=None,
conductor_api=None):
raise NotImplementedError()
# NOTE(danms): Here for quantum compatibility
def deallocate_port_for_instance(self, context, instance, port_id,
conductor_api=None):
raise NotImplementedError()
# NOTE(danms): Here for quantum compatibility
def list_ports(self, *args, **kwargs):
raise NotImplementedError()
# NOTE(danms): Here for quantum compatibility
def show_port(self, *args, **kwargs):
raise NotImplementedError()
@wrap_check_policy
@refresh_cache
def add_fixed_ip_to_instance(self, context, instance, network_id,

View File

@ -110,7 +110,7 @@ class API(base.Base):
return nets
def allocate_for_instance(self, context, instance, **kwargs):
"""Allocate all network resources for the instance.
"""Allocate network resources for the instance.
TODO(someone): document the rest of these parameters.
@ -230,6 +230,33 @@ class API(base.Base):
self.trigger_security_group_members_refresh(context, instance)
self.trigger_instance_remove_security_group_refresh(context, instance)
def allocate_port_for_instance(self, context, instance, port_id,
network_id=None, requested_ip=None,
conductor_api=None):
return self.allocate_for_instance(context, instance,
requested_networks=[(network_id, requested_ip, port_id)],
conductor_api=conductor_api)
def deallocate_port_for_instance(self, context, instance, port_id,
conductor_api=None):
try:
quantumv2.get_client(context).delete_port(port_id)
except Exception as ex:
LOG.exception(_("Failed to delete quantum port %(port_id)s ") %
locals())
self.trigger_security_group_members_refresh(context, instance)
self.trigger_instance_remove_security_group_refresh(context, instance)
return self.get_instance_nw_info(context, instance,
conductor_api=conductor_api)
def list_ports(self, context, **search_opts):
return quantumv2.get_client(context).list_ports(**search_opts)
def show_port(self, context, port_id):
return quantumv2.get_client(context).show_port(port_id)
def get_instance_nw_info(self, context, instance, networks=None,
conductor_api=None):
result = self._get_instance_nw_info(context, instance, networks)
@ -640,7 +667,7 @@ class API(base.Base):
data = quantumv2.get_client(context,
admin=True).list_ports(**search_opts)
ports = data.get('ports', [])
if not networks:
if networks is None:
networks = self._get_available_networks(context,
instance['project_id'])
else:

View File

@ -0,0 +1,244 @@
# Copyright 2012 SINA Inc.
# All Rights Reserved.
#
# 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.
from nova.api.openstack.compute.contrib import attach_interfaces
from nova.compute import api as compute_api
from nova import context
from nova import exception
from nova.network import api as network_api
from nova.openstack.common import cfg
from nova.openstack.common import jsonutils
from nova import test
import webob
from webob import exc
CONF = cfg.CONF
FAKE_UUID1 = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
FAKE_UUID2 = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
FAKE_PORT_ID1 = '11111111-1111-1111-1111-111111111111'
FAKE_PORT_ID2 = '22222222-2222-2222-2222-222222222222'
FAKE_PORT_ID3 = '33333333-3333-3333-3333-333333333333'
FAKE_NET_ID1 = '44444444-4444-4444-4444-444444444444'
FAKE_NET_ID2 = '55555555-5555-5555-5555-555555555555'
FAKE_NET_ID3 = '66666666-6666-6666-6666-666666666666'
port_data1 = {
"id": FAKE_PORT_ID1,
"network_id": FAKE_NET_ID1,
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "aa:aa:aa:aa:aa:aa",
"fixed_ips": ["10.0.1.2"],
"device_id": FAKE_UUID1,
}
port_data2 = {
"id": FAKE_PORT_ID2,
"network_id": FAKE_NET_ID2,
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "bb:bb:bb:bb:bb:bb",
"fixed_ips": ["10.0.2.2"],
"device_id": FAKE_UUID1,
}
port_data3 = {
"id": FAKE_PORT_ID3,
"network_id": FAKE_NET_ID3,
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "bb:bb:bb:bb:bb:bb",
"fixed_ips": ["10.0.2.2"],
"device_id": '',
}
fake_networks = [FAKE_NET_ID1, FAKE_NET_ID2]
ports = [port_data1, port_data2, port_data3]
def fake_list_ports(self, *args, **kwargs):
result = []
for port in ports:
if port['device_id'] == kwargs['device_id']:
result.append(port)
return {'ports': result}
def fake_show_port(self, context, port_id, **kwargs):
for port in ports:
if port['id'] == port_id:
return {'port': port}
def fake_attach_interface(self, context, instance, network_id, port_id,
requested_ip='192.168.1.3'):
if not network_id:
# if no network_id is given when add a port to an instance, use the
# first default network.
network_id = fake_networks[0]
if not port_id:
port_id = ports[fake_networks.index(network_id)]['id']
network_info = [
{'bridge': 'br-100',
'id': network_id,
'cidr': '192.168.1.0/24',
'vlan': '101',
'injected': 'False',
'multi_host': 'False',
'bridge_interface': 'bridge_interface'
},
{'label': 'fake_network',
'broadcast': '192.168.1.255',
'mac': '11:22:33:11:22:33',
'vif_uuid': port_id,
'rxtx_cap': 0,
'dns': '8.8.8.8',
'dhcp_server': '192.168.1.1',
'ips': {'ip': requested_ip,
'enabled': 1,
'netmask': '255.255.255.0',
'gateway': '192.168.1.254'}
}
]
return network_info
def fake_detach_interface(self, context, instance, port_id):
for port in ports:
if port['id'] == port_id:
return
raise exception.PortNotFound(port_id=port_id)
def fake_get_instance(self, context, intance_id):
return {}
class InterfaceAttachTests(test.TestCase):
def setUp(self):
super(InterfaceAttachTests, self).setUp()
self.flags(quantum_auth_strategy=None)
self.flags(quantum_url='http://anyhost/')
self.flags(quantum_url_timeout=30)
self.stubs.Set(network_api.API, 'show_port', fake_show_port)
self.stubs.Set(network_api.API, 'list_ports', fake_list_ports)
self.stubs.Set(compute_api.API, 'get', fake_get_instance)
self.context = context.get_admin_context()
self.expected_show = {'interfaceAttachment':
{'net_id': FAKE_NET_ID1,
'port_id': FAKE_PORT_ID1,
'mac_addr': port_data1['mac_address'],
'port_state': port_data1['status'],
'fixed_ips': port_data1['fixed_ips'],
}}
def test_show(self):
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/show')
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
result = attachments.show(req, FAKE_UUID1, FAKE_PORT_ID1)
self.assertEqual(self.expected_show, result)
def test_show_invalid(self):
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/show')
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPNotFound,
attachments.show, req, FAKE_UUID2, FAKE_PORT_ID1)
def test_delete(self):
self.stubs.Set(compute_api.API, 'detach_interface',
fake_detach_interface)
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/delete')
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
result = attachments.delete(req, FAKE_UUID1, FAKE_PORT_ID1)
self.assertEqual('202 Accepted', result.status)
def test_delete_interface_not_found(self):
self.stubs.Set(compute_api.API, 'detach_interface',
fake_detach_interface)
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/delete')
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPNotFound,
attachments.delete,
req,
FAKE_UUID1,
'invaid-port-id')
def test_attach_interface_without_network_id(self):
self.stubs.Set(compute_api.API, 'attach_interface',
fake_attach_interface)
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/attach')
req.method = 'POST'
body = jsonutils.dumps({'port_id': FAKE_PORT_ID1})
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
result = attachments.create(req, FAKE_UUID1, jsonutils.loads(req.body))
self.assertEqual(result['interfaceAttachment']['net_id'],
FAKE_NET_ID1)
def test_attach_interface_with_network_id(self):
self.stubs.Set(compute_api.API, 'attach_interface',
fake_attach_interface)
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/attach')
req.method = 'POST'
req.body = jsonutils.dumps({'interfaceAttachment':
{'net_id': FAKE_NET_ID2}})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
result = attachments.create(req, FAKE_UUID1, jsonutils.loads(req.body))
self.assertEqual(result['interfaceAttachment']['net_id'],
FAKE_NET_ID2)
def test_attach_interface_with_port_and_network_id(self):
self.stubs.Set(compute_api.API, 'attach_interface',
fake_attach_interface)
attachments = attach_interfaces.InterfaceAttachmentController()
req = webob.Request.blank('/v2/fake/os-interfaces/attach')
req.method = 'POST'
req.body = jsonutils.dumps({'interfaceAttachment':
{'port_id': FAKE_PORT_ID1,
'net_id': FAKE_NET_ID2}})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPBadRequest,
attachments.create, req, FAKE_UUID1,
jsonutils.loads(req.body))

View File

@ -62,6 +62,7 @@ from nova.tests.compute import fake_resource_tracker
from nova.tests.db import fakes as db_fakes
from nova.tests import fake_instance_actions
from nova.tests import fake_network
from nova.tests import fake_network_cache_model
from nova.tests.image import fake as fake_image
from nova.tests import matchers
from nova import utils
@ -5828,6 +5829,41 @@ class ComputeAPITestCase(BaseTestCase):
db.instance_destroy(self.context, instance['uuid'])
def test_attach_interface(self):
instance = {
'image_ref': 'foo',
}
self.mox.StubOutWithMock(compute_manager, '_get_image_meta')
self.mox.StubOutWithMock(self.compute.network_api,
'allocate_port_for_instance')
nwinfo = network_model.NetworkInfo()
nwinfo.append(fake_network_cache_model.new_vif())
network_id = nwinfo[0]['network']['id']
port_id = nwinfo[0]['id']
req_ip = '1.2.3.4'
self.compute.network_api.allocate_port_for_instance(
self.context, instance, port_id, network_id, req_ip,
self.compute.conductor_api).AndReturn(nwinfo)
compute_manager._get_image_meta(self.context, instance['image_ref'])
self.mox.ReplayAll()
network, mapping = self.compute.attach_interface(self.context,
instance,
network_id,
port_id,
req_ip)
self.assertEqual(network['id'], network_id)
return nwinfo, port_id
def test_detach_interface(self):
nwinfo, port_id = self.test_attach_interface()
self.stubs.Set(self.compute.network_api, 'get_instance_nw_info',
lambda *a, **k: nwinfo)
self.stubs.Set(self.compute.network_api,
'deallocate_port_for_instance',
lambda a, b, c, d: [])
self.compute.detach_interface(self.context, {}, port_id)
self.assertEqual(self.compute.driver._interfaces, {})
def test_attach_volume(self):
# Ensure instance can be soft rebooted.

View File

@ -105,6 +105,7 @@ policy_data = """
"compute_extension:admin_actions:migrate": "",
"compute_extension:aggregates": "",
"compute_extension:agents": "",
"compute_extension:attach_interfaces": "",
"compute_extension:baremetal_nodes": "",
"compute_extension:cells": "",
"compute_extension:certificates": "",

View File

@ -96,6 +96,14 @@
"namespace": "http://docs.openstack.org/compute/ext/agents/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-attach-interfaces",
"description": "Attach interface support.",
"links": [],
"name": "AttachInterfaces",
"namespace": "http://docs.openstack.org/compute/ext/interfaces/api/v1.1",
"updated": "2012-07-22T00:00:00+00:00"
},
{
"alias": "os-availability-zone",
"description": "%(text)s",

View File

@ -33,6 +33,9 @@
<extension alias="os-aggregates" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/aggregates/api/v1.1" name="Aggregates">
<description>%(text)s</description>
</extension>
<extension alias="os-attach-interfaces" updated="2012-07-22T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/interfaces/api/v1.1" name="AttachInterfaces">
<description>Attach interface support.</description>
</extension>
<extension alias="os-availability-zone" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone">
<description>%(text)s</description>
</extension>

View File

@ -0,0 +1,5 @@
{
"interfaceAttachment": {
"port_id": "ce531f90-199f-48c0-816c-13e38010b442"
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachment>
<port_id>%(port_id)s</port_id>
</interfaceAttachment>

View File

@ -0,0 +1,12 @@
{
"interfaceAttachment": {
"fixed_ips": [{
"subnet_id": "%(subnet_id)s",
"ip_address": "%(ip_address)s"
}],
"mac_addr": "fa:16:3e:4c:2c:30",
"net_id": "%(net_id)s",
"port_id": "%(port_id)s",
"port_state": "%(port_state)s"
}
}

View File

@ -0,0 +1,12 @@
<interfaceAttachment>
<net_id>%(net_id)s</net_id>
<port_id>%(port_id)s</port_id>
<fixed_ips>
<fixed_ip>
<subnet_id>%(subnet_id)s</subnet_id>
<ip_address>%(ip_address)s</ip_address>
</fixed_ip>
</fixed_ips>
<port_state>%(port_state)s</port_state>
<mac_addr>%(mac_addr)s</mac_addr>
</interfaceAttachment>

View File

@ -0,0 +1,16 @@
{
"interfaceAttachments": [
{
"port_state": "%(port_state)s",
"fixed_ips": [
{
"subnet_id": "%(subnet_id)s",
"ip_address": "%(ip_address)s"
}
],
"net_id": "%(net_id)s",
"port_id": "%(port_id)s",
"mac_addr": "%(mac_addr)s"
}
]
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachments>
<interfaceAttachment>
<port_state>%(port_state)s</port_state>
<fixed_ips>
<fixed_ip>
<subnet_id>%(subnet_id)s</subnet_id>
<ip_address>%(ip_address)s</ip_address>
</fixed_ip>
</fixed_ips>
<port_id>%(port_id)s</port_id>
<net_id>%(net_id)s</net_id>
<mac_addr>%(mac_addr)s</mac_addr>
</interfaceAttachment>
</interfaceAttachments>

View File

@ -0,0 +1,14 @@
{
"interfaceAttachment": {
"port_state": "%(port_state)s",
"fixed_ips": [
{
"subnet_id": "%(subnet_id)s",
"ip_address": "%(ip_address)s"
}
],
"net_id": "%(net_id)s",
"port_id": "%(port_id)s",
"mac_addr": "%(mac_addr)s"
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceAttachment>
<port_state>%(port_state)s</port_state>
<fixed_ips>
<fixed_ip>
<subnet_id>%(subnet_id)s</subnet_id>
<ip_address>%(ip_address)s</ip_address>
</fixed_ip>
</fixed_ips>
<port_id>%(port_id)s</port_id>
<net_id>%(net_id)s</net_id>
<mac_addr>%(mac_addr)s</mac_addr>
</interfaceAttachment>

View File

@ -0,0 +1,16 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "%(host)s/openstack/images/%(image_id)s",
"flavorRef" : "%(host)s/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
]
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
<metadata/>
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
</server>

View File

@ -31,6 +31,7 @@ from nova.api.openstack.compute.contrib import coverage_ext
from nova.api.openstack.compute.contrib import fping
# Import extensions to pull in osapi_compute_extension CONF option used below.
from nova.cloudpipe import pipelib
from nova.compute import api as compute_api
from nova import context
from nova import db
from nova.db.sqlalchemy import models
@ -266,7 +267,6 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
sample_data = "{}"
else:
sample_data = None
try:
response_result = self._verify_something(subs, expected,
response_data)
@ -3302,3 +3302,174 @@ class FlavorAccessSampleJsonTests(ApiSampleTestBase):
class FlavorAccessSampleXmlTests(FlavorAccessSampleJsonTests):
ctype = "xml"
class AttachInterfacesSampleJsonTest(ServersSampleBase):
extension_name = ('nova.api.openstack.compute.contrib.attach_interfaces.'
'Attach_interfaces')
def setUp(self):
super(AttachInterfacesSampleJsonTest, self).setUp()
def fake_list_ports(self, *args, **kwargs):
uuid = kwargs.get('device_id', None)
if not uuid:
raise InstanceNotFound(instance_id=None)
port_data = {
"id": "ce531f90-199f-48c0-816c-13e38010b442",
"network_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "fa:16:3e:4c:2c:30",
"fixed_ips": [
{
"ip_address": "192.168.1.3",
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}
],
"device_id": uuid,
}
ports = {'ports': [port_data]}
return ports
def fake_show_port(self, context, port_id=None):
if not port_id:
raise PortNotFound(port_id=None)
port_data = {
"id": port_id,
"network_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "fa:16:3e:4c:2c:30",
"fixed_ips": [
{
"ip_address": "192.168.1.3",
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}
],
"device_id": 'bece68a3-2f8b-4e66-9092-244493d6aba7',
}
port = {'port': port_data}
return port
def fake_attach_interface(self, context, instance,
network_id, port_id,
requested_ip='192.168.1.3'):
if not network_id:
network_id = "fake_net_uuid"
if not port_id:
port_id = "fake_port_uuid"
network_info = [
{
'bridge': 'br-100',
'id': network_id,
'cidr': '192.168.1.0/24',
'vlan': '101',
'injected': 'False',
'multi_host': 'False',
'bridge_interface': 'bridge_interface'
},
{
"vif_uuid": port_id,
"network_id": network_id,
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "fa:16:3e:4c:2c:30",
"fixed_ips": [
{
"ip_address": requested_ip,
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}
],
"device_id": instance['uuid'],
}
]
return network_info
def fake_detach_interface(self, context, instance, port_id):
pass
self.stubs.Set(network_api.API, 'list_ports', fake_list_ports)
self.stubs.Set(network_api.API, 'show_port', fake_show_port)
self.stubs.Set(compute_api.API, 'attach_interface',
fake_attach_interface)
self.stubs.Set(compute_api.API, 'detach_interface',
fake_detach_interface)
self.flags(quantum_auth_strategy=None)
self.flags(quantum_url='http://anyhost/')
self.flags(quantum_url_timeout=30)
def generalize_subs(self, subs, vanilla_regexes):
subs['subnet_id'] = vanilla_regexes['uuid']
subs['net_id'] = vanilla_regexes['uuid']
subs['port_id'] = vanilla_regexes['uuid']
subs['mac_addr'] = '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}'
subs['ip_address'] = vanilla_regexes['ip']
return subs
def test_list_interfaces(self):
instance_uuid = self._post_server()
response = self._do_get('servers/%s/os-interface' % instance_uuid)
self.assertEqual(response.status, 200)
subs = {
'ip_address': '192.168.1.3',
'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef',
'mac_addr': 'fa:16:3e:4c:2c:30',
'net_id': '3cb9bc59-5699-4588-a4b1-b87f96708bc6',
'port_id': 'ce531f90-199f-48c0-816c-13e38010b442',
'port_state': 'ACTIVE'
}
self._verify_response('attach-interfaces-list-resp', subs, response)
def _stub_show_for_instance(self, instance_uuid, port_id):
show_port = network_api.API().show_port(None, port_id)
show_port['port']['device_id'] = instance_uuid
self.stubs.Set(network_api.API, 'show_port', lambda *a, **k: show_port)
def test_show_interfaces(self):
instance_uuid = self._post_server()
port_id = 'ce531f90-199f-48c0-816c-13e38010b442'
self._stub_show_for_instance(instance_uuid, port_id)
response = self._do_get('servers/%s/os-interface/%s' %
(instance_uuid, port_id))
self.assertEqual(response.status, 200)
subs = {
'ip_address': '192.168.1.3',
'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef',
'mac_addr': 'fa:16:3e:4c:2c:30',
'net_id': '3cb9bc59-5699-4588-a4b1-b87f96708bc6',
'port_id': port_id,
'port_state': 'ACTIVE'
}
self._verify_response('attach-interfaces-show-resp', subs, response)
def test_create_interfaces(self, instance_uuid=None):
if instance_uuid is None:
instance_uuid = self._post_server()
subs = {
'net_id': '3cb9bc59-5699-4588-a4b1-b87f96708bc6',
'port_id': 'ce531f90-199f-48c0-816c-13e38010b442',
'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef',
'ip_address': '192.168.1.3',
'port_state': 'ACTIVE',
'mac_addr': 'fa:16:3e:4c:2c:30',
}
self._stub_show_for_instance(instance_uuid, subs['port_id'])
response = self._do_post('servers/%s/os-interface' % instance_uuid,
'attach-interfaces-create-req', subs)
self.assertEqual(response.status, 200)
subs.update(self._get_regexes())
self._verify_response('attach-interfaces-create-resp',
subs, response)
def test_delete_interfaces(self):
instance_uuid = self._post_server()
port_id = 'ce531f90-199f-48c0-816c-13e38010b442'
response = self._do_delete('servers/%s/os-interface/%s' %
(instance_uuid, port_id))
self.assertEqual(response.status, 202)
self.assertEqual(response.read(), '')
class AttachInterfacesSampleXmlTest(AttachInterfacesSampleJsonTest):
ctype = 'xml'

View File

@ -230,11 +230,10 @@ class TestQuantumv2(test.TestCase):
'router_id': 'router_id1'}
def tearDown(self):
try:
self.mox.UnsetStubs()
self.mox.VerifyAll()
finally:
CONF.reset()
self.addCleanup(CONF.reset)
self.addCleanup(self.mox.VerifyAll)
self.addCleanup(self.mox.UnsetStubs)
self.addCleanup(self.stubs.UnsetAll)
super(TestQuantumv2, self).tearDown()
def _verify_nw_info(self, nw_inf, index=0):
@ -614,7 +613,9 @@ class TestQuantumv2(test.TestCase):
{'ports': port_data})
for port in port_data:
self.moxed_client.delete_port(port['id'])
self.mox.ReplayAll()
api = quantumapi.API()
api.deallocate_for_instance(self.context, self.instance)
@ -626,6 +627,56 @@ class TestQuantumv2(test.TestCase):
# Test to deallocate in two ports env.
self._deallocate_for_instance(2)
def _test_deallocate_port_for_instance(self, number):
port_data = number == 1 and self.port_data1 or self.port_data2
self.moxed_client.delete_port(port_data[0]['id'])
nets = [port_data[0]['network_id']]
quantumv2.get_client(mox.IgnoreArg(), admin=True).AndReturn(
self.moxed_client)
self.moxed_client.list_ports(
tenant_id=self.instance['project_id'],
device_id=self.instance['uuid']).AndReturn(
{'ports': port_data[1:]})
quantumv2.get_client(mox.IgnoreArg()).MultipleTimes().AndReturn(
self.moxed_client)
self.moxed_client.list_networks(
tenant_id=self.instance['project_id'],
shared=False).AndReturn(
{'networks': [self.nets2[1]]})
self.moxed_client.list_networks(shared=True).AndReturn(
{'networks': []})
for port in port_data[1:]:
self.moxed_client.list_subnets(id=['my_subid2']).AndReturn({})
self.mox.ReplayAll()
api = quantumapi.API()
nwinfo = api.deallocate_port_for_instance(self.context, self.instance,
port_data[0]['id'])
self.assertEqual(len(nwinfo), len(port_data[1:]))
if len(port_data) > 1:
self.assertEqual(nwinfo[0]['network']['id'], 'my_netid2')
def test_deallocate_port_for_instance_1(self):
# Test to deallocate the first and only port
self._test_deallocate_port_for_instance(1)
def test_deallocate_port_for_instance_2(self):
# Test to deallocate the first port of two
self._test_deallocate_port_for_instance(2)
def test_list_ports(self):
search_opts = {'parm': 'value'}
self.moxed_client.list_ports(**search_opts)
self.mox.ReplayAll()
quantumapi.API().list_ports(self.context, **search_opts)
def test_show_port(self):
self.moxed_client.show_port('foo')
self.mox.ReplayAll()
quantumapi.API().show_port(self.context, 'foo')
def test_validate_networks(self):
requested_networks = [('my_netid1', 'test', None),
('my_netid2', 'test2', None)]

View File

@ -302,6 +302,14 @@ class ComputeDriver(object):
"""Detach the disk attached to the instance."""
raise NotImplementedError()
def attach_interface(self, instance, image_meta, network_info):
"""Attach an interface to the instance."""
raise NotImplementedError()
def detach_interface(self, instance, network_info):
"""Detach an interface from the instance."""
raise NotImplementedError()
def migrate_disk_and_power_off(self, context, instance, dest,
instance_type, network_info,
block_device_info=None):

View File

@ -102,6 +102,7 @@ class FakeDriver(driver.ComputeDriver):
'hypervisor_hostname': 'fake-mini',
}
self._mounts = {}
self._interfaces = {}
def init_host(self, host):
return
@ -222,6 +223,19 @@ class FakeDriver(driver.ComputeDriver):
pass
return True
def attach_interface(self, instance, image_meta, network_info):
for (network, mapping) in network_info:
if mapping['vif_uuid'] in self._interfaces:
raise exception.InterfaceAttachFailed('duplicate')
self._interfaces[mapping['vif_uuid']] = mapping
def detach_interface(self, instance, network_info):
for (network, mapping) in network_info:
try:
del self._interfaces[mapping['vif_uuid']]
except KeyError:
raise exception.InterfaceDetachFailed('not attached')
def get_info(self, instance):
if instance['name'] not in self.instances:
raise exception.InstanceNotFound(instance_id=instance['name'])

View File

@ -767,6 +767,50 @@ class LibvirtDriver(driver.ComputeDriver):
connection_info,
disk_dev)
@exception.wrap_exception()
def attach_interface(self, instance, image_meta, network_info):
virt_dom = self._lookup_by_name(instance['name'])
for (network, mapping) in network_info:
self.vif_driver.plug(instance, (network, mapping))
self.firewall_driver.setup_basic_filtering(instance,
[(network, mapping)])
cfg = self.vif_driver.get_config(instance, network, mapping,
image_meta)
try:
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
state = LIBVIRT_POWER_STATE[virt_dom.info()[0]]
if state == power_state.RUNNING:
flags |= libvirt.VIR_DOMAIN_AFFECT_LIVE
virt_dom.attachDeviceFlags(cfg.to_xml(), flags)
except libvirt.libvirtError as ex:
LOG.error(_('attaching network adapter failed.'),
instance=instance)
self.vif_driver.unplug(instance, (network, mapping))
raise exception.InterfaceAttachFailed(instance)
@exception.wrap_exception()
def detach_interface(self, instance, network_info):
virt_dom = self._lookup_by_name(instance['name'])
for (network, mapping) in network_info:
cfg = self.vif_driver.get_config(instance, network, mapping, None)
try:
self.vif_driver.unplug(instance, (network, mapping))
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
state = LIBVIRT_POWER_STATE[virt_dom.info()[0]]
if state == power_state.RUNNING:
flags |= libvirt.VIR_DOMAIN_AFFECT_LIVE
virt_dom.detachDeviceFlags(cfg.to_xml(), flags)
except libvirt.libvirtError as ex:
error_code = ex.get_error_code()
if error_code == libvirt.VIR_ERR_NO_DOMAIN:
LOG.warn(_("During detach_interface, "
"instance disappeared."),
instance=instance)
else:
LOG.error(_('detaching network adapter failed.'),
instance=instance)
raise exception.InterfaceDetachFailed(instance)
def snapshot(self, context, instance, image_href, update_task_state):
"""Create snapshot from a running VM instance.