diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json
index ba5e410eb880..0009c5e837e9 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.json
+++ b/doc/api_samples/all_extensions/extensions-get-resp.json
@@ -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",
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml
index a18e52437045..d479d09b57ef 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.xml
+++ b/doc/api_samples/all_extensions/extensions-get-resp.xml
@@ -40,6 +40,9 @@
Admin-only aggregate administration.
+
+ Attach interface support.
+
1. Add availability_zone to the Create Server v1.1 API.
2. Add availability zones describing.
@@ -88,8 +91,8 @@
Instance deferred delete.
-
- Enables server evacuation
+
+ Enables server evacuation.
Fixed IPs support.
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.json b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.json
new file mode 100644
index 000000000000..11dcf64373a0
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.json
@@ -0,0 +1,5 @@
+{
+ "interfaceAttachment": {
+ "port_id": "ce531f90-199f-48c0-816c-13e38010b442"
+ }
+}
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml
new file mode 100644
index 000000000000..bd3f24265368
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml
@@ -0,0 +1,4 @@
+
+
+ ce531f90-199f-48c0-816c-13e38010b442
+
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json
new file mode 100644
index 000000000000..309f2a1e8e58
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json
@@ -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"
+ }
+}
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml
new file mode 100644
index 000000000000..4b3254371788
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml
@@ -0,0 +1,13 @@
+
+
+ 3cb9bc59-5699-4588-a4b1-b87f96708bc6
+ ce531f90-199f-48c0-816c-13e38010b442
+
+
+ f8a6e8f8-c2ec-497c-9f23-da9616de54ef
+ 192.168.1.3
+
+
+ ACTIVE
+ fa:16:3e:4c:2c:30
+
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json b/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json
new file mode 100644
index 000000000000..2c62ef9d5da2
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json
@@ -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"
+ }
+ ]
+}
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml b/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml
new file mode 100644
index 000000000000..f1bef407cc0e
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml
@@ -0,0 +1,15 @@
+
+
+
+ ACTIVE
+
+
+ f8a6e8f8-c2ec-497c-9f23-da9616de54ef
+ 192.168.1.3
+
+
+ ce531f90-199f-48c0-816c-13e38010b442
+ 3cb9bc59-5699-4588-a4b1-b87f96708bc6
+ fa:16:3e:4c:2c:30
+
+
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-list.xml b/doc/api_samples/os-attach-interfaces/attach-interfaces-list.xml
new file mode 100644
index 000000000000..3392e2cc658f
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-list.xml
@@ -0,0 +1,17 @@
+
+
+
+ -
+ ACTIVE
+
+
+ f8a6e8f8-c2ec-497c-9f23-da9616de54ef
+ 192.168.1.3
+
+
+ ce531f90-199f-48c0-816c-13e38010b442
+ 3cb9bc59-5699-4588-a4b1-b87f96708bc6
+ fa:16:3e:4c:2c:30
+
+
+
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json b/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json
new file mode 100644
index 000000000000..14f5b9e1ed4c
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json
@@ -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"
+ }
+}
diff --git a/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml b/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml
new file mode 100644
index 000000000000..ce3e2fec29a9
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml
@@ -0,0 +1,13 @@
+
+
+ ACTIVE
+
+
+ b6e47749-6bf0-4d6e-ae4b-ba6b5e238510
+ 192.168.123.131
+
+
+ 89e64f2e-86bd-4c19-9155-4548b36fdcb2
+ a9efd207-2c1a-4cdd-a296-d3c7c3211302
+ fa:16:3e:a4:1c:12
+
diff --git a/doc/api_samples/os-attach-interfaces/server-post-req.json b/doc/api_samples/os-attach-interfaces/server-post-req.json
new file mode 100644
index 000000000000..d88eb4122223
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/server-post-req.json
@@ -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=="
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/server-post-req.xml b/doc/api_samples/os-attach-interfaces/server-post-req.xml
new file mode 100644
index 000000000000..0a3c8bb5303d
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/server-post-req.xml
@@ -0,0 +1,19 @@
+
+
+
+ Apache1
+
+
+
+ ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
+ dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
+ IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
+ c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
+ QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
+ ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
+ dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
+ c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
+ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
+
+
+
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/server-post-resp.json b/doc/api_samples/os-attach-interfaces/server-post-resp.json
new file mode 100644
index 000000000000..54e47aefa015
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/server-post-resp.json
@@ -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"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-attach-interfaces/server-post-resp.xml b/doc/api_samples/os-attach-interfaces/server-post-resp.xml
new file mode 100644
index 000000000000..0efd9029d527
--- /dev/null
+++ b/doc/api_samples/os-attach-interfaces/server-post-resp.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 2d3c4ed062ad..6098773bb079 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -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": "",
diff --git a/nova/api/openstack/compute/contrib/attach_interfaces.py b/nova/api/openstack/compute/contrib/attach_interfaces.py
new file mode 100644
index 000000000000..a838354d01fb
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/attach_interfaces.py
@@ -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
diff --git a/nova/compute/api.py b/nova/compute/api.py
index f83243e8b799..d7e85d0350c5 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -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."""
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 9d4fc0a4645b..92cf3cb00693 100755
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -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)
diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py
index 525d1adc741b..0b45b1f63dd1 100644
--- a/nova/compute/rpcapi.py
+++ b/nova/compute/rpcapi.py
@@ -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',
diff --git a/nova/exception.py b/nova/exception.py
index 9e9e5182b497..cdc533d2b39d 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -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 "
diff --git a/nova/network/api.py b/nova/network/api.py
index 09c4a8c79fa0..98387687de9f 100644
--- a/nova/network/api.py
+++ b/nova/network/api.py
@@ -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,
diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py
index ee4ceb9cdf77..445e158f504a 100644
--- a/nova/network/quantumv2/api.py
+++ b/nova/network/quantumv2/api.py
@@ -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:
diff --git a/nova/tests/api/openstack/compute/contrib/test_attach_interfaces.py b/nova/tests/api/openstack/compute/contrib/test_attach_interfaces.py
new file mode 100644
index 000000000000..462e4375ce92
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_attach_interfaces.py
@@ -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))
diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py
index d1a7952f3543..0dfb6621832b 100644
--- a/nova/tests/compute/test_compute.py
+++ b/nova/tests/compute/test_compute.py
@@ -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.
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index 3878df531d99..95052043760e 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -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": "",
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
index 17914de426d4..910867e8a6b0 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
@@ -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",
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
index 4492ed3aaaae..cfd85c5bcad9 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
@@ -33,6 +33,9 @@
%(text)s
+
+ Attach interface support.
+
%(text)s
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.json.tpl
new file mode 100644
index 000000000000..11dcf64373a0
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.json.tpl
@@ -0,0 +1,5 @@
+{
+ "interfaceAttachment": {
+ "port_id": "ce531f90-199f-48c0-816c-13e38010b442"
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml.tpl
new file mode 100644
index 000000000000..75e9b97c8ca3
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-req.xml.tpl
@@ -0,0 +1,4 @@
+
+
+ %(port_id)s
+
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json.tpl
new file mode 100644
index 000000000000..d882cdc61239
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.json.tpl
@@ -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"
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml.tpl
new file mode 100644
index 000000000000..b391e5973334
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-create-resp.xml.tpl
@@ -0,0 +1,12 @@
+
+ %(net_id)s
+ %(port_id)s
+
+
+ %(subnet_id)s
+ %(ip_address)s
+
+
+ %(port_state)s
+ %(mac_addr)s
+
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json.tpl
new file mode 100644
index 000000000000..47dcf2dc64c0
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.json.tpl
@@ -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"
+ }
+ ]
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml.tpl
new file mode 100644
index 000000000000..f3262e948e8f
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-list-resp.xml.tpl
@@ -0,0 +1,15 @@
+
+
+
+ %(port_state)s
+
+
+ %(subnet_id)s
+ %(ip_address)s
+
+
+ %(port_id)s
+ %(net_id)s
+ %(mac_addr)s
+
+
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json.tpl
new file mode 100644
index 000000000000..3333bb49991a
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.json.tpl
@@ -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"
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml.tpl
new file mode 100644
index 000000000000..a3393448d4aa
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/attach-interfaces-show-resp.xml.tpl
@@ -0,0 +1,13 @@
+
+
+ %(port_state)s
+
+
+ %(subnet_id)s
+ %(ip_address)s
+
+
+ %(port_id)s
+ %(net_id)s
+ %(mac_addr)s
+
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.json.tpl
new file mode 100644
index 000000000000..d3916d1aa68a
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.json.tpl
@@ -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=="
+ }
+ ]
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.xml.tpl
new file mode 100644
index 000000000000..f92614984242
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-req.xml.tpl
@@ -0,0 +1,19 @@
+
+
+
+ Apache1
+
+
+
+ ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
+ dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
+ IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
+ c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
+ QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
+ ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
+ dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
+ c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
+ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
+
+
+
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.json.tpl
new file mode 100644
index 000000000000..d5f030c8730b
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.json.tpl
@@ -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"
+ }
+ ]
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.xml.tpl
new file mode 100644
index 000000000000..3bb13e69bd6d
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-attach-interfaces/server-post-resp.xml.tpl
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py
index e179052d67cc..bc95e8357bb9 100644
--- a/nova/tests/integrated/test_api_samples.py
+++ b/nova/tests/integrated/test_api_samples.py
@@ -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'
diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py
index f3f306694da0..2e2504c819d4 100644
--- a/nova/tests/network/test_quantumv2.py
+++ b/nova/tests/network/test_quantumv2.py
@@ -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)]
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index ba0dfbafec2f..994f85ec1f76 100755
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -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):
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 30a5fc75885b..5545dcf96746 100755
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -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'])
diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py
index 386fe836cb5b..24de509e220a 100755
--- a/nova/virt/libvirt/driver.py
+++ b/nova/virt/libvirt/driver.py
@@ -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.