]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add new attributes to EIPAssociation resource
authorJeff Peeler <jpeeler@redhat.com>
Wed, 3 Jul 2013 14:22:25 +0000 (10:22 -0400)
committerJeff Peeler <jpeeler@redhat.com>
Wed, 21 Aug 2013 15:10:11 +0000 (11:10 -0400)
AllocationId
NetworkInterfaceId

The EIP resource also now supports the Domain property, which when set
creates the resource using neutron networking. (Neutron networking is
required for all VPC operations.)

The EIP test covers the new functionality as well as some of the old
using both nova and neutron.

Fixes bug #1164865
Fixes bug #1164866

Change-Id: I7ad7fba8ed6f8a584fc2ca00aaf80144c5274134

heat/engine/resources/eip.py
heat/tests/test_eip.py

index d6c9e5c9218334d73c4adbdf5c13d0df5b7bb21e..b8e8df4bfc8316738cfa990f72fc324bdfe2766b 100644 (file)
@@ -15,6 +15,8 @@
 
 from heat.engine import clients
 from heat.engine import resource
+from heat.engine.resources.vpc import VPC
+from heat.common import exception
 
 from heat.openstack.common import excutils
 from heat.openstack.common import log as logging
@@ -24,7 +26,7 @@ logger = logging.getLogger(__name__)
 
 class ElasticIp(resource.Resource):
     properties_schema = {'Domain': {'Type': 'String',
-                                    'Implemented': False},
+                         'AllowedValues': ['vpc']},
                          'InstanceId': {'Type': 'String'}}
     attributes_schema = {
         "AllocationId": ("ID that AWS assigns to represent the allocation of"
@@ -37,8 +39,17 @@ class ElasticIp(resource.Resource):
         self.ipaddress = None
 
     def _ipaddress(self):
-        if self.ipaddress is None:
-            if self.resource_id is not None:
+        if self.ipaddress is None and self.resource_id is not None:
+            if self.properties['Domain'] and clients.neutronclient:
+                ne = clients.neutronclient.exceptions.NeutronClientException
+                try:
+                    ips = self.neutron().show_floatingip(self.resource_id)
+                except ne as e:
+                    if e.status_code == 404:
+                        logger.warn("Floating IPs not found: %s" % str(e))
+                else:
+                    self.ipaddress = ips['floatingip']['floating_ip_address']
+            else:
                 try:
                     ips = self.nova().floating_ips.get(self.resource_id)
                 except clients.novaclient.exceptions.NotFound as ex:
@@ -49,18 +60,34 @@ class ElasticIp(resource.Resource):
 
     def handle_create(self):
         """Allocate a floating IP for the current tenant."""
-        try:
-            ips = self.nova().floating_ips.create()
-        except clients.novaclient.exceptions.NotFound:
-            with excutils.save_and_reraise_exception():
-                msg = ("No default floating IP pool configured."
-                       "Set 'default_floating_pool' in nova.conf.")
-                logger.error(msg)
-
-        if ips:
+        ips = None
+        if self.properties['Domain'] and clients.neutronclient:
+            from heat.engine.resources.internet_gateway import InternetGateway
+
+            ext_net = InternetGateway.get_external_network_id(self.neutron())
+            props = {'floating_network_id': ext_net}
+            ips = self.neutron().create_floatingip({
+                'floatingip': props})['floatingip']
+            self.ipaddress = ips['floating_ip_address']
+            self.resource_id_set(ips['id'])
             logger.info('ElasticIp create %s' % str(ips))
-            self.ipaddress = ips.ip
-            self.resource_id_set(ips.id)
+        else:
+            if self.properties['Domain']:
+                raise exception.Error('Domain property can not be set on '
+                                      'resource %s without Neutron available' %
+                                      self.name)
+            try:
+                ips = self.nova().floating_ips.create()
+            except clients.novaclient.exceptions.NotFound:
+                with excutils.save_and_reraise_exception():
+                    msg = ("No default floating IP pool configured."
+                           "Set 'default_floating_pool' in nova.conf.")
+                    logger.error(msg)
+
+            if ips:
+                self.ipaddress = ips.ip
+                self.resource_id_set(ips.id)
+                logger.info('ElasticIp create %s' % str(ips))
 
         if self.properties['InstanceId']:
             server = self.nova().servers.get(self.properties['InstanceId'])
@@ -77,7 +104,15 @@ class ElasticIp(resource.Resource):
 
         """De-allocate a floating IP."""
         if self.resource_id is not None:
-            self.nova().floating_ips.delete(self.resource_id)
+            if self.properties['Domain'] and clients.neutronclient:
+                ne = clients.neutronclient.exceptions.NeutronClientException
+                try:
+                    self.neutron().delete_floatingip(self.resource_id)
+                except ne as e:
+                    if e.status_code != 404:
+                        raise e
+            else:
+                self.nova().floating_ips.delete(self.resource_id)
 
     def FnGetRefId(self):
         return unicode(self._ipaddress())
@@ -91,32 +126,82 @@ class ElasticIpAssociation(resource.Resource):
     properties_schema = {'InstanceId': {'Type': 'String',
                                         'Required': False},
                          'EIP': {'Type': 'String'},
-                         'AllocationId': {'Type': 'String',
-                                          'Implemented': False}}
+                         'AllocationId': {'Type': 'String'},
+                         'NetworkInterfaceId': {'Type': 'String'}}
 
     def FnGetRefId(self):
-        return unicode(self.properties.get('EIP', '0.0.0.0'))
+        return unicode(self.physical_resource_name())
 
     def handle_create(self):
         """Add a floating IP address to a server."""
-        logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' %
-                     (self.properties['InstanceId'],
-                      self.properties['EIP']))
-
-        if self.properties['InstanceId']:
+        if self.properties['EIP'] is not None \
+                and self.properties['AllocationId'] is not None:
+                    raise exception.ResourcePropertyConflict('EIP',
+                                                             'AllocationId')
+
+        if self.properties['EIP']:
+            if not self.properties['InstanceId']:
+                logger.warn('Skipping association, InstanceId not specified')
+                return
             server = self.nova().servers.get(self.properties['InstanceId'])
             server.add_floating_ip(self.properties['EIP'])
-        self.resource_id_set(self.properties['EIP'])
+            self.resource_id_set(self.properties['EIP'])
+            logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' %
+                         (self.properties['InstanceId'],
+                          self.properties['EIP']))
+        elif self.properties['AllocationId']:
+            assert clients.neutronclient, "Neutron required for VPC operations"
+            port_id = None
+            port_rsrc = None
+            if self.properties['NetworkInterfaceId']:
+                port_id = self.properties['NetworkInterfaceId']
+                port_rsrc = self.neutron().list_ports(id=port_id)['ports'][0]
+            elif self.properties['InstanceId']:
+                instance_id = self.properties['InstanceId']
+                ports = self.neutron().list_ports(device_id=instance_id)
+                port_rsrc = ports['ports'][0]
+                port_id = port_rsrc['id']
+            else:
+                logger.warn('Skipping association, resource not specified')
+                return
+
+            float_id = self.properties['AllocationId']
+            self.resource_id_set(float_id)
+
+            # assuming only one fixed_ip
+            subnet_id = port_rsrc['fixed_ips'][0]['subnet_id']
+            subnets = self.neutron().list_subnets(id=subnet_id)
+            subnet_rsrc = subnets['subnets'][0]
+            netid = subnet_rsrc['network_id']
+
+            router_id = VPC.router_for_vpc(self.neutron(), netid)['id']
+            floatingip = self.neutron().show_floatingip(float_id)
+            floating_net_id = floatingip['floatingip']['floating_network_id']
+
+            self.neutron().add_gateway_router(
+                router_id, {'network_id': floating_net_id})
+
+            self.neutron().update_floatingip(
+                float_id, {'floatingip': {'port_id': port_id}})
 
     def handle_delete(self):
-        """Remove a floating IP address from a server."""
-        if self.properties['InstanceId']:
+        """Remove a floating IP address from a server or port."""
+        if self.properties['EIP']:
             try:
                 server = self.nova().servers.get(self.properties['InstanceId'])
                 if server:
                     server.remove_floating_ip(self.properties['EIP'])
             except clients.novaclient.exceptions.NotFound as ex:
                 pass
+        elif self.properties['AllocationId']:
+            float_id = self.properties['AllocationId']
+            ne = clients.neutronclient.exceptions.NeutronClientException
+            try:
+                self.neutron().update_floatingip(
+                    float_id, {'floatingip': {'port_id': None}})
+            except ne as e:
+                if e.status_code != 404:
+                    raise e
 
 
 def resource_mapping():
index 75f4c66669b3b0f51399f5e88b0f57014e22630b..728af8872d6eaa4faa821aa3bcd0f0e929f168e0 100644 (file)
@@ -12,6 +12,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from testtools import skipIf
 
 from heat.common import exception
 from heat.common import template_format
@@ -19,8 +20,10 @@ from heat.engine.resources import eip
 from heat.engine import clients
 from heat.engine import resource
 from heat.engine import scheduler
+from heat.engine import parser
 from heat.tests.common import HeatTestCase
 from heat.tests.v1_1 import fakes
+from heat.tests import fakes as fakec
 from heat.tests import utils
 
 
@@ -66,9 +69,63 @@ eip_template_ipassoc = '''
 }
 '''
 
+eip_template_ipassoc2 = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "EIP Test",
+  "Parameters" : {},
+  "Resources" : {
+    "the_eip" : {
+      "Type" : "AWS::EC2::EIP",
+      "Properties" : {
+        "Domain": "vpc"
+      }
+    },
+    "IPAssoc" : {
+      "Type" : "AWS::EC2::EIPAssociation",
+      "Properties" : {
+        "AllocationId" : 'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
+        "NetworkInterfaceId" : { "Ref" : "the_nic" }
+      }
+    },
+    "the_vpc" : {
+      "Type" : "AWS::EC2::VPC",
+      "Properties" : {
+        "CidrBlock" : "10.0.0.0/16"
+      }
+    },
+    "the_subnet" : {
+      "Type" : "AWS::EC2::Subnet",
+      "Properties" : {
+        "CidrBlock" : "10.0.0.0/24",
+        "VpcId" : { "Ref" : "the_vpc" }
+      }
+    },
+    "the_nic" : {
+      "Type" : "AWS::EC2::NetworkInterface",
+      "Properties" : {
+        "PrivateIpAddress": "10.0.0.100",
+        "SubnetId": { "Ref": "the_subnet" }
+      }
+    },
+  }
+}
+'''
+
+
+def force_networking(mode):
+    if mode == 'nova':
+        force_networking.client = clients.neutronclient
+        clients.neutronclient = None
+    if mode == 'neutron':
+        clients.neutronclient = force_networking.client
+force_networking.client = None
+
 
 class EIPTest(HeatTestCase):
     def setUp(self):
+        # force Nova, will test Neutron below
+        force_networking('nova')
         super(EIPTest, self).setUp()
         self.fc = fakes.FakeClient()
         self.m.StubOutWithMock(eip.ElasticIp, 'nova')
@@ -76,6 +133,10 @@ class EIPTest(HeatTestCase):
         self.m.StubOutWithMock(self.fc.servers, 'get')
         utils.setup_dummy_db()
 
+    def tearDown(self):
+        super(EIPTest, self).tearDown()
+        force_networking('neutron')
+
     def create_eip(self, t, stack, resource_name):
         rsrc = eip.ElasticIp(resource_name,
                              t['Resources'][resource_name],
@@ -95,7 +156,6 @@ class EIPTest(HeatTestCase):
         return rsrc
 
     def test_eip(self):
-
         eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc)
         self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0])
         self.fc.servers.get('WebServer')
@@ -109,7 +169,7 @@ class EIPTest(HeatTestCase):
 
         try:
             self.assertEqual('11.0.0.1', rsrc.FnGetRefId())
-            rsrc.ipaddress = None
+            rsrc.refid = None
             self.assertEqual('11.0.0.1', rsrc.FnGetRefId())
 
             self.assertEqual('1', rsrc.FnGetAtt('AllocationId'))
@@ -125,6 +185,26 @@ class EIPTest(HeatTestCase):
 
         self.m.VerifyAll()
 
+    def test_association_eip(self):
+        eip.ElasticIp.nova().AndReturn(self.fc)
+        eip.ElasticIp.nova().AndReturn(self.fc)
+
+        self.m.ReplayAll()
+
+        t = template_format.parse(eip_template_ipassoc)
+        stack = utils.parse_stack(t)
+
+        rsrc = self.create_eip(t, stack, 'IPAddress')
+        association = self.create_association(t, stack, 'IPAssoc')
+
+        # TODO(sbaker), figure out why this is an empty string
+        #self.assertEqual('', association.FnGetRefId())
+
+        association.delete()
+        rsrc.delete()
+
+        self.m.VerifyAll()
+
     def test_eip_with_exception(self):
         self.m.StubOutWithMock(self.fc.floating_ips, 'create')
         eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc)
@@ -143,24 +223,230 @@ class EIPTest(HeatTestCase):
                           rsrc.handle_create)
         self.m.VerifyAll()
 
-    def test_association(self):
-        eip.ElasticIp.nova().AndReturn(self.fc)
-        eip.ElasticIpAssociation.nova().AndReturn(self.fc)
-        self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0])
-        eip.ElasticIpAssociation.nova().AndReturn(self.fc)
+
+class AllocTest(HeatTestCase):
+
+    @skipIf(clients.neutronclient is None, 'neutronclient unavailable')
+    def setUp(self):
+        super(AllocTest, self).setUp()
+
+        self.fc = fakes.FakeClient()
+        self.m.StubOutWithMock(eip.ElasticIp, 'nova')
+        self.m.StubOutWithMock(eip.ElasticIpAssociation, 'nova')
+        self.m.StubOutWithMock(self.fc.servers, 'get')
+
+        self.m.StubOutWithMock(parser.Stack, 'resource_by_refid')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'create_floatingip')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'show_floatingip')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'update_floatingip')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'delete_floatingip')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'add_gateway_router')
+        self.m.StubOutWithMock(clients.neutronclient.Client, 'list_networks')
+        self.m.StubOutWithMock(clients.neutronclient.Client, 'list_ports')
+        self.m.StubOutWithMock(clients.neutronclient.Client, 'list_subnets')
+        self.m.StubOutWithMock(clients.neutronclient.Client, 'show_network')
+        self.m.StubOutWithMock(clients.neutronclient.Client, 'list_routers')
+        self.m.StubOutWithMock(clients.neutronclient.Client,
+                               'remove_gateway_router')
+        self.m.StubOutWithMock(clients.OpenStackClients, 'keystone')
+
+        utils.setup_dummy_db()
+
+    def mock_show_network(self):
+        vpc_name = utils.PhysName('test_stack', 'the_vpc')
+        clients.neutronclient.Client.show_network(
+            'aaaa-netid'
+        ).AndReturn({"network": {
+            "status": "BUILD",
+            "subnets": [],
+            "name": vpc_name,
+            "admin_state_up": False,
+            "shared": False,
+            "tenant_id": "c1210485b2424d48804aad5d39c61b8f",
+            "id": "aaaa-netid"
+        }})
+
+    def create_eip(self, t, stack, resource_name):
+        rsrc = eip.ElasticIp(resource_name,
+                             t['Resources'][resource_name],
+                             stack)
+        self.assertEqual(None, rsrc.validate())
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
+        return rsrc
+
+    def create_association(self, t, stack, resource_name):
+        rsrc = eip.ElasticIpAssociation(resource_name,
+                                        t['Resources'][resource_name],
+                                        stack)
+        self.assertEqual(None, rsrc.validate())
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
+        return rsrc
+
+    def mock_update_floatingip(self, port='the_nic'):
+        clients.neutronclient.Client.update_floatingip(
+            'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
+            {'floatingip': {'port_id': port}}).AndReturn(None)
+
+    def mock_create_gateway_attachment(self):
+        clients.neutronclient.Client.add_gateway_router(
+            'bbbb', {'network_id': 'eeee'}).AndReturn(None)
+
+    def mock_create_floatingip(self):
+        clients.neutronclient.Client.list_networks(
+            **{'router:external': True}).AndReturn({'networks': [{
+                'status': 'ACTIVE',
+                'subnets': [],
+                'name': 'nova',
+                'router:external': True,
+                'tenant_id': 'c1210485b2424d48804aad5d39c61b8f',
+                'admin_state_up': True,
+                'shared': True,
+                'id': 'eeee'
+            }]})
+
+        clients.neutronclient.Client.create_floatingip({
+            'floatingip': {'floating_network_id': u'eeee'}
+        }).AndReturn({'floatingip': {
+            "status": "ACTIVE",
+            "id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766",
+            "floating_ip_address": "192.168.9.3"
+        }})
+
+    def mock_show_floatingip(self, refid):
+        clients.neutronclient.Client.show_floatingip(
+            refid,
+        ).AndReturn({'floatingip': {
+            'router_id': None,
+            'tenant_id': 'e936e6cd3e0b48dcb9ff853a8f253257',
+            'floating_network_id': 'eeee',
+            'fixed_ip_address': None,
+            'floating_ip_address': '172.24.4.227',
+            'port_id': None,
+            'id': 'ffff'
+        }})
+
+    def mock_delete_floatingip(self):
+        id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766'
+        clients.neutronclient.Client.delete_floatingip(id).AndReturn(None)
+
+    def mock_list_ports(self):
+        clients.neutronclient.Client.list_ports(id='the_nic').AndReturn(
+            {"ports": [{
+                "status": "DOWN",
+                "binding:host_id": "null",
+                "name": "wp-NIC-yu7fc7l4g5p6",
+                "admin_state_up": True,
+                "network_id": "22c26451-cf27-4d48-9031-51f5e397b84e",
+                "tenant_id": "ecf538ec1729478fa1f97f1bf4fdcf7b",
+                "binding:vif_type": "ovs",
+                "device_owner": "",
+                "binding:capabilities": {"port_filter": True},
+                "mac_address": "fa:16:3e:62:2d:4f",
+                "fixed_ips": [{"subnet_id": "mysubnetid-70ec",
+                               "ip_address": "192.168.9.2"}],
+                "id": "a000228d-b40b-4124-8394-a4082ae1b76b",
+                "security_groups": ["5c6f529d-3186-4c36-84c0-af28b8daac7b"],
+                "device_id": ""
+            }]})
+
+    def mock_list_subnets(self):
+        clients.neutronclient.Client.list_subnets(
+            id='mysubnetid-70ec').AndReturn(
+                {'subnets': [{
+                    u'name': u'wp-Subnet-pyjm7bvoi4xw',
+                    u'enable_dhcp': True,
+                    u'network_id': u'aaaa-netid',
+                    u'tenant_id': u'ecf538ec1729478fa1f97f1bf4fdcf7b',
+                    u'dns_nameservers': [],
+                    u'allocation_pools': [{u'start': u'192.168.9.2',
+                                           u'end': u'192.168.9.254'}],
+                    u'host_routes': [],
+                    u'ip_version': 4,
+                    u'gateway_ip': u'192.168.9.1',
+                    u'cidr': u'192.168.9.0/24',
+                    u'id': u'2c339ccd-734a-4acc-9f64-6f0dfe427e2d'
+                }]})
+
+    def mock_router_for_vpc(self):
+        vpc_name = utils.PhysName('test_stack', 'the_vpc')
+        clients.neutronclient.Client.list_routers(name=vpc_name).AndReturn({
+            "routers": [{
+                "status": "ACTIVE",
+                "external_gateway_info": {
+                    "network_id": "zzzz",
+                    "enable_snat": True},
+                "name": vpc_name,
+                "admin_state_up": True,
+                "tenant_id": "3e21026f2dc94372b105808c0e721661",
+                "routes": [],
+                "id": "bbbb"
+            }]
+        })
+
+    def mock_keystone(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakec.FakeKeystoneClient())
+
+    def test_neutron_eip(self):
+        eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc)
         self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0])
-        eip.ElasticIp.nova().AndReturn(self.fc)
+        self.fc.servers.get('WebServer')
 
         self.m.ReplayAll()
 
-        t = template_format.parse(eip_template_ipassoc)
+        t = template_format.parse(eip_template)
         stack = utils.parse_stack(t)
 
         rsrc = self.create_eip(t, stack, 'IPAddress')
-        association = self.create_association(t, stack, 'IPAssoc')
 
-        # TODO(sbaker), figure out why this is an empty string
-        #self.assertEqual('', association.FnGetRefId())
+        try:
+            self.assertEqual('11.0.0.1', rsrc.FnGetRefId())
+            rsrc.refid = None
+            self.assertEqual('11.0.0.1', rsrc.FnGetRefId())
+
+            self.assertEqual('1', rsrc.FnGetAtt('AllocationId'))
+
+            self.assertRaises(resource.UpdateReplace,
+                              rsrc.handle_update, {}, {}, {})
+
+            self.assertRaises(exception.InvalidTemplateAttribute,
+                              rsrc.FnGetAtt, 'Foo')
+
+        finally:
+            rsrc.destroy()
+
+        self.m.VerifyAll()
+
+    def test_association_allocationid(self):
+        self.mock_keystone()
+        self.mock_create_gateway_attachment()
+        self.mock_show_network()
+        self.mock_router_for_vpc()
+
+        self.mock_create_floatingip()
+        self.mock_list_ports()
+        self.mock_list_subnets()
+
+        self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766')
+        self.mock_update_floatingip()
+
+        self.mock_update_floatingip(port=None)
+        self.mock_delete_floatingip()
+
+        self.m.ReplayAll()
+
+        t = template_format.parse(eip_template_ipassoc2)
+        stack = utils.parse_stack(t)
+
+        rsrc = self.create_eip(t, stack, 'the_eip')
+        association = self.create_association(t, stack, 'IPAssoc')
 
         association.delete()
         rsrc.delete()