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.