diff --git a/bin/nova-manage b/bin/nova-manage index a61b5c1dd369..79683fef7b25 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1,6 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -103,6 +104,8 @@ flags.DECLARE('multi_host', 'nova.network.manager') flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('vlan_start', 'nova.network.manager') flags.DECLARE('vpn_start', 'nova.network.manager') +flags.DECLARE('default_floating_pool', 'nova.network.manager') +flags.DECLARE('public_interface', 'nova.network.linux_net') flags.DECLARE('libvirt_type', 'nova.virt.libvirt.connection') flags.DEFINE_flag(flags.HelpFlag()) flags.DEFINE_flag(flags.HelpshortFlag()) @@ -684,14 +687,23 @@ class FixedIpCommands(object): class FloatingIpCommands(object): """Class for managing floating ip.""" - @args('--ip_range', dest="range", metavar='', help='IP range') - def create(self, range): + @args('--ip_range', dest="ip_range", metavar='', help='IP range') + @args('--pool', dest="pool", metavar='', help='Optional pool') + @args('--interface', dest="interface", metavar='', + help='Optional interface') + def create(self, ip_range, pool=None, interface=None): """Creates floating ips for zone by range""" - addresses = netaddr.IPNetwork(range) + addresses = netaddr.IPNetwork(ip_range) admin_context = context.get_admin_context() + if not pool: + pool = FLAGS.default_floating_pool + if not interface: + interface = FLAGS.public_interface for address in addresses.iter_hosts(): db.floating_ip_create(admin_context, - {'address': str(address)}) + {'address': str(address), + 'pool': pool, + 'interface': interface}) @args('--ip_range', dest="ip_range", metavar='', help='IP range') def delete(self, ip_range): diff --git a/nova/api/openstack/v2/contrib/floating_ip_pools.py b/nova/api/openstack/v2/contrib/floating_ip_pools.py new file mode 100644 index 000000000000..9d6386f25fa0 --- /dev/null +++ b/nova/api/openstack/v2/contrib/floating_ip_pools.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# +# 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 import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack.v2 import extensions +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_poolss') + + +def _translate_floating_ip_view(pool): + return { + 'name': pool['name'], + } + + +def _translate_floating_ip_pools_view(pools): + return { + 'floating_ip_pools': [_translate_floating_ip_view(pool) + for pool in pools] + } + + +class FloatingIPPoolsController(object): + """The Floating IP Pool API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPPoolsController, self).__init__() + + def index(self, req): + """Return a list of pools.""" + context = req.environ['nova.context'] + pools = self.network_api.get_floating_ip_pools(context) + return _translate_floating_ip_pools_view(pools) + + +def make_float_ip(elem): + elem.set('name') + + +class FloatingIPPoolTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pool', + selector='floating_ip_pool') + make_float_ip(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pools') + elem = xmlutil.SubTemplateElement(root, 'floating_ip_pool', + selector='floating_ip_pools') + make_float_ip(elem) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return FloatingIPPoolsTemplate() + + +class Floating_ip_pools(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ip_pools" + alias = "os-floating-ip-pools" + namespace = \ + "http://docs.openstack.org/compute/ext/floating_ip_pools/api/v1.1" + updated = "2012-01-04T00:00:00+00:00" + + def get_resources(self): + resources = [] + + body_serializers = { + 'application/xml': FloatingIPPoolsSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + res = extensions.ResourceExtension('os-floating-ip-pools', + FloatingIPPoolsController(), + serializer=serializer, + member_actions={}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/v2/contrib/floating_ips.py b/nova/api/openstack/v2/contrib/floating_ips.py index 39700d3829e3..3a6f4ee348a3 100644 --- a/nova/api/openstack/v2/contrib/floating_ips.py +++ b/nova/api/openstack/v2/contrib/floating_ips.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2011 Grid Dynamics # Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev # @@ -34,6 +35,7 @@ LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ips') def make_float_ip(elem): elem.set('id') elem.set('ip') + elem.set('pool') elem.set('fixed_ip') elem.set('instance_id') @@ -56,8 +58,11 @@ class FloatingIPsTemplate(xmlutil.TemplateBuilder): def _translate_floating_ip_view(floating_ip): - result = {'id': floating_ip['id'], - 'ip': floating_ip['address']} + result = { + 'id': floating_ip['id'], + 'ip': floating_ip['address'], + 'pool': floating_ip['pool'], + } try: result['fixed_ip'] = floating_ip['fixed_ip']['address'] except (TypeError, KeyError): @@ -106,13 +111,19 @@ class FloatingIPController(object): def create(self, req, body=None): context = req.environ['nova.context'] + pool = None + if body and 'pool' in body: + pool = body['pool'] try: - address = self.network_api.allocate_floating_ip(context) + address = self.network_api.allocate_floating_ip(context, pool) ip = self.network_api.get_floating_ip_by_address(context, address) except rpc.RemoteError as ex: # NOTE(tr3buchet) - why does this block exist? if ex.exc_type == 'NoMoreFloatingIps': - msg = _("No more floating ips available.") + if pool: + msg = _("No more floating ips in pool %s.") % pool + else: + msg = _("No more floating ips available.") raise webob.exc.HTTPBadRequest(explanation=msg) else: raise diff --git a/nova/db/api.py b/nova/db/api.py index e3ba9a1b5a38..5f9f9f43e531 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -237,13 +238,18 @@ def floating_ip_get(context, id): return IMPL.floating_ip_get(context, id) -def floating_ip_allocate_address(context, project_id): - """Allocate free floating ip and return the address. +def floating_ip_get_pools(context): + """Returns a list of floating ip pools""" + return IMPL.floating_ip_get_pools(context) + + +def floating_ip_allocate_address(context, project_id, pool): + """Allocate free floating ip from specified pool and return the address. Raises if one is not available. """ - return IMPL.floating_ip_allocate_address(context, project_id) + return IMPL.floating_ip_allocate_address(context, project_id, pool) def floating_ip_create(context, values): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index eba393256c17..83a022292a20 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -489,7 +490,16 @@ def floating_ip_get(context, id): @require_context -def floating_ip_allocate_address(context, project_id): +def floating_ip_get_pools(context): + session = get_session() + pools = [] + for result in session.query(models.FloatingIp.pool).distinct(): + pools.append({'name': result[0]}) + return pools + + +@require_context +def floating_ip_allocate_address(context, project_id, pool): authorize_project_context(context, project_id) session = get_session() with session.begin(): @@ -497,6 +507,7 @@ def floating_ip_allocate_address(context, project_id): session=session, read_deleted="no").\ filter_by(fixed_ip_id=None).\ filter_by(project_id=None).\ + filter_by(pool=pool).\ with_lockmode('update').\ first() # NOTE(vish): if with_lockmode isn't supported, as in sqlite, diff --git a/nova/db/sqlalchemy/migrate_repo/versions/067_add_pool_and_interface_to_floating_ip.py b/nova/db/sqlalchemy/migrate_repo/versions/067_add_pool_and_interface_to_floating_ip.py new file mode 100644 index 000000000000..205d7e03440d --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/067_add_pool_and_interface_to_floating_ip.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay 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 sqlalchemy import * + +from sqlalchemy import Column, MetaData, Table, String + +from nova import flags + + +flags.DECLARE('default_floating_pool', 'nova.network.manager') +flags.DECLARE('public_interface', 'nova.network.linux_net') +FLAGS = flags.FLAGS + +meta = MetaData() + +pool_column = Column('pool', String(255)) +interface_column = Column('interface', String(255)) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + table = Table('floating_ips', meta, autoload=True) + table.create_column(pool_column) + table.create_column(interface_column) + table.update().values(pool=FLAGS.default_floating_pool, + interface=FLAGS.public_interface).execute() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + table = Table('floating_ips', meta, autoload=True) + table.c.pool.drop() + table.c.interface.drop() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 4a27e80343d6..fad41334ee1e 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 Piston Cloud Computing, Inc. @@ -711,6 +712,8 @@ class FloatingIp(BASE, NovaBase): project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) auto_assigned = Column(Boolean, default=False, nullable=False) + pool = Column(String(255)) + interface = Column(String(255)) class AuthToken(BASE, NovaBase): diff --git a/nova/network/api.py b/nova/network/api.py index 64292eb0406e..82b3739a39d7 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -39,6 +40,11 @@ class API(base.Base): {'method': 'get_floating_ip', 'args': {'id': id}}) + def get_floating_ip_pools(self, context): + return rpc.call(context, + FLAGS.network_topic, + {'method': 'get_floating_pools'}) + def get_floating_ip_by_address(self, context, address): return rpc.call(context, FLAGS.network_topic, @@ -62,8 +68,8 @@ class API(base.Base): {'method': 'get_vifs_by_instance', 'args': {'instance_id': instance_id}}) - def allocate_floating_ip(self, context): - """Adds a floating ip to a project. (allocates)""" + def allocate_floating_ip(self, context, pool=None): + """Adds a floating ip to a project from a pool. (allocates)""" # NOTE(vish): We don't know which network host should get the ip # when we allocate, so just send it to any one. This # will probably need to move into a network supervisor @@ -71,7 +77,8 @@ class API(base.Base): return rpc.call(context, FLAGS.network_topic, {'method': 'allocate_floating_ip', - 'args': {'project_id': context.project_id}}) + 'args': {'project_id': context.project_id, + 'pool': pool}}) def release_floating_ip(self, context, address, affect_auto_assigned=False): diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 3784a11179de..5b0c80eec839 100755 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -432,21 +433,21 @@ def init_host(ip_range=None): iptables_manager.apply() -def bind_floating_ip(floating_ip, check_exit_code=True): +def bind_floating_ip(floating_ip, device, check_exit_code=True): """Bind ip to public interface.""" _execute('ip', 'addr', 'add', str(floating_ip) + '/32', - 'dev', FLAGS.public_interface, + 'dev', device, run_as_root=True, check_exit_code=check_exit_code) if FLAGS.send_arp_for_ha: _execute('arping', '-U', floating_ip, - '-A', '-I', FLAGS.public_interface, + '-A', '-I', device, '-c', 1, run_as_root=True, check_exit_code=False) -def unbind_floating_ip(floating_ip): +def unbind_floating_ip(floating_ip, device): """Unbind a public ip from public interface.""" _execute('ip', 'addr', 'del', str(floating_ip) + '/32', - 'dev', FLAGS.public_interface, run_as_root=True) + 'dev', device, run_as_root=True) def ensure_metadata_ip(): diff --git a/nova/network/manager.py b/nova/network/manager.py index ecea7806b27d..eff312f5b460 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -94,6 +95,8 @@ flags.DEFINE_integer('network_size', 256, 'Number of addresses in each private subnet') flags.DEFINE_string('floating_range', '4.4.4.0/24', 'Floating IP address block') +flags.DEFINE_string('default_floating_pool', 'nova', + 'Default pool for floating ips') flags.DEFINE_string('fixed_range', '10.0.0.0/8', 'Fixed IP address block') flags.DEFINE_string('fixed_range_v6', 'fd00::/48', 'Fixed IPv6 address block') flags.DEFINE_string('gateway', None, 'Default IPv4 gateway') @@ -201,7 +204,9 @@ class FloatingIP(object): fixed_address = floating_ip['fixed_ip']['address'] # NOTE(vish): The False here is because we ignore the case # that the ip is already bound. - self.driver.bind_floating_ip(floating_ip['address'], False) + self.driver.bind_floating_ip(floating_ip['address'], + floating_ip['interface'], + False) self.driver.ensure_floating_forward(floating_ip['address'], fixed_address) @@ -286,8 +291,8 @@ class FloatingIP(object): 'project': context.project_id}) raise exception.NotAuthorized() - def allocate_floating_ip(self, context, project_id): - """Gets an floating ip from the pool.""" + def allocate_floating_ip(self, context, project_id, pool=None): + """Gets a floating ip from the pool.""" # NOTE(tr3buchet): all network hosts in zone now use the same pool LOG.debug("QUOTA: %s" % quota.allowed_floating_ips(context, 1)) if quota.allowed_floating_ips(context, 1) < 1: @@ -296,9 +301,10 @@ class FloatingIP(object): context.project_id) raise exception.QuotaError(_('Address quota exceeded. You cannot ' 'allocate any more addresses')) - # TODO(vish): add floating ips through manage command + pool = pool or FLAGS.default_floating_pool return self.db.floating_ip_allocate_address(context, - project_id) + project_id, + pool) def deallocate_floating_ip(self, context, address, affect_auto_assigned=False): @@ -337,7 +343,6 @@ class FloatingIP(object): # make sure floating ip isn't already associated if floating_ip['fixed_ip_id']: - floating_address = floating_ip['address'] raise exception.FloatingIpAssociated(address=floating_address) fixed_ip = self.db.fixed_ip_get_by_address(context, fixed_address) @@ -348,20 +353,22 @@ class FloatingIP(object): host = instance['host'] else: host = fixed_ip['network']['host'] - LOG.info("%s", self.host) + interface = floating_ip['interface'] if host == self.host: # i'm the correct host self._associate_floating_ip(context, floating_address, - fixed_address) + fixed_address, interface) else: # send to correct host rpc.cast(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': '_associate_floating_ip', - 'args': {'floating_address': floating_ip['address'], - 'fixed_address': fixed_ip['address']}}) + 'args': {'floating_address': floating_address, + 'fixed_address': fixed_address, + 'interface': interface}}) - def _associate_floating_ip(self, context, floating_address, fixed_address): + def _associate_floating_ip(self, context, floating_address, fixed_address, + interface): """Performs db and driver calls to associate floating ip & fixed ip""" # associate floating ip self.db.floating_ip_fixed_ip_associate(context, @@ -369,7 +376,7 @@ class FloatingIP(object): fixed_address, self.host) # gogo driver time - self.driver.bind_floating_ip(floating_address) + self.driver.bind_floating_ip(floating_address, interface) self.driver.ensure_floating_forward(floating_address, fixed_address) def disassociate_floating_ip(self, context, address, @@ -401,29 +408,36 @@ class FloatingIP(object): host = instance['host'] else: host = fixed_ip['network']['host'] + interface = floating_ip['interface'] if host == self.host: # i'm the correct host - self._disassociate_floating_ip(context, address) + self._disassociate_floating_ip(context, address, interface) else: # send to correct host rpc.cast(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': '_disassociate_floating_ip', - 'args': {'address': address}}) + 'args': {'address': address, + 'interface': interface}}) - def _disassociate_floating_ip(self, context, address): + def _disassociate_floating_ip(self, context, address, interface): """Performs db and driver calls to disassociate floating ip""" # disassociate floating ip fixed_address = self.db.floating_ip_disassociate(context, address) # go go driver time - self.driver.unbind_floating_ip(address) + self.driver.unbind_floating_ip(address, interface) self.driver.remove_floating_forward(address, fixed_address) def get_floating_ip(self, context, id): """Returns a floating IP as a dict""" return dict(self.db.floating_ip_get(context, id).iteritems()) + def get_floating_pools(self, context): + """Returns list of floating pools""" + pools = self.db.floating_ip_get_pools(context) + return [dict(pool.iteritems()) for pool in pools] + def get_floating_ip_by_address(self, context, address): """Returns a floating IP as a dict""" return dict(self.db.floating_ip_get_by_address(context, diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index 421c16adb832..6015069bcf12 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -153,7 +154,7 @@ class CloudTestCase(test.TestCase): address = "10.10.10.10" db.floating_ip_create(self.context, {'address': address, - 'host': self.network.host}) + 'pool': 'nova'}) self.cloud.allocate_address(self.context) self.cloud.describe_addresses(self.context) self.cloud.release_address(self.context, @@ -165,7 +166,7 @@ class CloudTestCase(test.TestCase): allocate = self.cloud.allocate_address db.floating_ip_create(self.context, {'address': address, - 'host': self.network.host}) + 'pool': 'nova'}) self.assertEqual(allocate(self.context)['publicIp'], address) db.floating_ip_destroy(self.context, address) self.assertRaises(exception.NoMoreFloatingIps, @@ -177,7 +178,7 @@ class CloudTestCase(test.TestCase): allocate = self.cloud.allocate_address db.floating_ip_create(self.context, {'address': address, - 'host': self.network.host, + 'pool': 'nova', 'project_id': self.project_id}) result = self.cloud.release_address(self.context, address) self.assertEqual(result['releaseResponse'], ['Address released.']) @@ -185,7 +186,9 @@ class CloudTestCase(test.TestCase): def test_associate_disassociate_address(self): """Verifies associate runs cleanly without raising an exception""" address = "10.10.10.10" - db.floating_ip_create(self.context, {'address': address}) + db.floating_ip_create(self.context, + {'address': address, + 'pool': 'nova'}) self.cloud.allocate_address(self.context) # TODO(jkoelker) Probably need to query for instance_type_id and # make sure we get a valid one diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py b/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py new file mode 100644 index 000000000000..d061f9af3658 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py @@ -0,0 +1,73 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay 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 lxml import etree + +from nova.api.openstack.v2.contrib import floating_ip_pools +from nova import context +from nova import network +from nova import test +from nova.tests.api.openstack import fakes + + +def fake_get_floating_ip_pools(self, context): + return [{'name': 'nova'}, + {'name': 'other'}] + + +class FloatingIpPoolTest(test.TestCase): + def setUp(self): + super(FloatingIpPoolTest, self).setUp() + self.stubs.Set(network.api.API, "get_floating_ip_pools", + fake_get_floating_ip_pools) + + self.context = context.RequestContext('fake', 'fake') + self.controller = floating_ip_pools.FloatingIPPoolsController() + + def test_translate_floating_ip_pools_view(self): + pools = fake_get_floating_ip_pools(None, self.context) + view = floating_ip_pools._translate_floating_ip_pools_view(pools) + self.assertTrue('floating_ip_pools' in view) + self.assertEqual(view['floating_ip_pools'][0]['name'], + pools[0]['name']) + self.assertEqual(view['floating_ip_pools'][1]['name'], + pools[1]['name']) + + def test_floating_ips_pools_list(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ip-pools') + res_dict = self.controller.index(req) + + pools = fake_get_floating_ip_pools(None, self.context) + response = {'floating_ip_pools': pools} + self.assertEqual(res_dict, response) + + +class FloatingIpPoolSerializerTest(test.TestCase): + def test_index_serializer(self): + serializer = floating_ip_pools.FloatingIPPoolsSerializer() + text = serializer.serialize(dict( + floating_ip_pools=[ + dict(name='nova'), + dict(name='other') + ]), 'index') + + tree = etree.fromstring(text) + + self.assertEqual('floating_ip_pools', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual('floating_ip_pool', tree[0].tag) + self.assertEqual('floating_ip_pool', tree[1].tag) + self.assertEqual('nova', tree[0].get('name')) + self.assertEqual('other', tree[1].get('name')) diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ips.py b/nova/tests/api/openstack/v2/contrib/test_floating_ips.py index d1a0d5795c79..ffdace53c683 100644 --- a/nova/tests/api/openstack/v2/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/v2/contrib/test_floating_ips.py @@ -1,3 +1,4 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2011 Eldar Nugaev # All Rights Reserved. # @@ -30,11 +31,13 @@ FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' def network_api_get_floating_ip(self, context, id): return {'id': 1, 'address': '10.10.10.10', + 'pool': 'nova', 'fixed_ip': None} def network_api_get_floating_ip_by_address(self, context, address): return {'id': 1, 'address': '10.10.10.10', + 'pool': 'nova', 'fixed_ip': {'address': '10.0.0.1', 'instance': {'uuid': FAKE_UUID}}} @@ -42,9 +45,11 @@ def network_api_get_floating_ip_by_address(self, context, address): def network_api_get_floating_ips_by_project(self, context): return [{'id': 1, 'address': '10.10.10.10', + 'pool': 'nova', 'fixed_ip': {'address': '10.0.0.1', 'instance': {'uuid': FAKE_UUID}}}, {'id': 2, + 'pool': 'nova', 'interface': 'eth0', 'address': '10.10.10.11'}] @@ -107,6 +112,7 @@ class FloatingIpTest(test.TestCase): host = "fake_host" return db.floating_ip_create(self.context, {'address': self.floating_ip, + 'pool': 'nova', 'host': host}) def _delete_floating_ip(self): @@ -151,7 +157,8 @@ class FloatingIpTest(test.TestCase): self.assertEqual(view['floating_ip']['instance_id'], None) def test_translate_floating_ip_view_dict(self): - floating_ip = {'id': 0, 'address': '10.0.0.10', 'fixed_ip': None} + floating_ip = {'id': 0, 'address': '10.0.0.10', 'pool': 'nova', + 'fixed_ip': None} view = floating_ips._translate_floating_ip_view(floating_ip) self.assertTrue('floating_ip' in view) @@ -161,10 +168,12 @@ class FloatingIpTest(test.TestCase): response = {'floating_ips': [{'instance_id': FAKE_UUID, 'ip': '10.10.10.10', + 'pool': 'nova', 'fixed_ip': '10.0.0.1', 'id': 1}, {'instance_id': None, 'ip': '10.10.10.11', + 'pool': 'nova', 'fixed_ip': None, 'id': 2}]} self.assertEqual(res_dict, response) @@ -180,6 +189,7 @@ class FloatingIpTest(test.TestCase): def test_show_associated_floating_ip(self): def get_floating_ip(self, context, id): return {'id': 1, 'address': '10.10.10.10', + 'pool': 'nova', 'fixed_ip': {'address': '10.0.0.1', 'instance': {'uuid': FAKE_UUID}}} self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) @@ -207,7 +217,7 @@ class FloatingIpTest(test.TestCase): pass def fake2(*args, **kwargs): - return {'id': 1, 'address': '10.10.10.10'} + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova'} self.stubs.Set(network.api.API, "allocate_floating_ip", fake1) @@ -223,7 +233,8 @@ class FloatingIpTest(test.TestCase): "id": 1, "instance_id": None, "ip": "10.10.10.10", - "fixed_ip": None} + "fixed_ip": None, + "pool": 'nova'} self.assertEqual(ip, expected) def test_floating_ip_release(self): diff --git a/nova/tests/api/openstack/v2/test_extensions.py b/nova/tests/api/openstack/v2/test_extensions.py index 51af89da3fd8..70a936727dc8 100644 --- a/nova/tests/api/openstack/v2/test_extensions.py +++ b/nova/tests/api/openstack/v2/test_extensions.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2011 OpenStack LLC. # All Rights Reserved. # @@ -110,6 +111,7 @@ class ExtensionControllerTest(ExtensionTestCase): "FlavorExtraData", "Floating_ips", "Floating_ip_dns", + "Floating_ip_pools", "Fox In Socks", "Hosts", "Keypairs", diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py index a28d6ded0e8d..ef3162eb4ca5 100644 --- a/nova/tests/db/fakes.py +++ b/nova/tests/db/fakes.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 OpenStack, LLC # All Rights Reserved. # @@ -88,6 +89,7 @@ def stub_out_db_network_api(stubs): 'fixed_ip_id': None, 'fixed_ip': None, 'project_id': None, + 'pool': 'nova', 'auto_assigned': False} virtual_interface_fields = {'id': 0, @@ -101,9 +103,10 @@ def stub_out_db_network_api(stubs): virtual_interfacees = [virtual_interface_fields] networks = [network_fields] - def fake_floating_ip_allocate_address(context, project_id): + def fake_floating_ip_allocate_address(context, project_id, pool): ips = filter(lambda i: i['fixed_ip_id'] is None \ - and i['project_id'] is None, + and i['project_id'] is None \ + and i['pool'] == pool, floating_ips) if not ips: raise exception.NoMoreFloatingIps() diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 4eee3c8d444f..274d5dd86062 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 Rackspace +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -108,6 +109,8 @@ flavor = {'id': 0, floating_ip_fields = {'id': 0, 'address': '192.168.10.100', + 'pool': 'nova', + 'interface': 'eth0', 'fixed_ip_id': 0, 'project_id': None, 'auto_assigned': False} @@ -580,21 +583,29 @@ class VlanNetworkTestCase(test.TestCase): # floating ip that's already associated def fake2(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'fixed_ip_id': 1} # floating ip that isn't associated def fake3(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'fixed_ip_id': None} # fixed ip with remote host def fake4(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'network': {'multi_host': False, 'host': 'jibberjabber'}} # fixed ip with local host def fake5(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'network': {'multi_host': False, 'host': 'testhost'}} def fake6(*args, **kwargs): @@ -641,21 +652,29 @@ class VlanNetworkTestCase(test.TestCase): # floating ip that isn't associated def fake2(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'fixed_ip_id': None} # floating ip that is associated def fake3(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'fixed_ip_id': 1} # fixed ip with remote host def fake4(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'network': {'multi_host': False, 'host': 'jibberjabber'}} # fixed ip with local host def fake5(*args, **kwargs): return {'address': '10.0.0.1', + 'pool': 'nova', + 'interface': 'eth0', 'network': {'multi_host': False, 'host': 'testhost'}} def fake6(*args, **kwargs):