diff --git a/heat/engine/clients/os/neutron.py b/heat/engine/clients/os/neutron.py index 3e482e2aa2..16f5849401 100644 --- a/heat/engine/clients/os/neutron.py +++ b/heat/engine/clients/os/neutron.py @@ -14,6 +14,7 @@ from neutronclient.common import exceptions from neutronclient.neutron import v2_0 as neutronV20 from neutronclient.v2_0 import client as nc +from oslo_utils import netutils from oslo_utils import uuidutils from heat.common import exception @@ -172,3 +173,10 @@ class SubnetConstraint(constraints.BaseCustomConstraint): neutron_client = client.client('neutron') neutronV20.find_resourceid_by_name_or_id( neutron_client, 'subnet', value) + + +class IPConstraint(constraints.BaseCustomConstraint): + + def validate(self, value, context): + self._error_message = 'Invalid IP address' + return netutils.is_valid_ip(value) diff --git a/heat/engine/resources/aws/ec2/eip.py b/heat/engine/resources/aws/ec2/eip.py index 8fa37fdd55..f99c8f5077 100644 --- a/heat/engine/resources/aws/ec2/eip.py +++ b/heat/engine/resources/aws/ec2/eip.py @@ -201,7 +201,10 @@ class ElasticIpAssociation(resource.Resource): EIP: properties.Schema( properties.Schema.STRING, _('EIP address to associate with instance.'), - update_allowed=True + update_allowed=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), ALLOCATION_ID: properties.Schema( properties.Schema.STRING, diff --git a/heat/engine/resources/openstack/neutron/floatingip.py b/heat/engine/resources/openstack/neutron/floatingip.py index 5f3dc4fe32..cfa3bda62d 100644 --- a/heat/engine/resources/openstack/neutron/floatingip.py +++ b/heat/engine/resources/openstack/neutron/floatingip.py @@ -78,13 +78,19 @@ class FloatingIP(neutron.NeutronResource): FIXED_IP_ADDRESS: properties.Schema( properties.Schema.STRING, _('IP address to use if the port has multiple addresses.'), - update_allowed=True + update_allowed=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), FLOATING_IP_ADDRESS: properties.Schema( properties.Schema.STRING, _('IP address of the floating IP. NOTE: The default policy ' 'setting in Neutron restricts usage of this property to ' 'administrative users only.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ], support_status=support.SupportStatus(version='2015.2'), ), } @@ -257,7 +263,10 @@ class FloatingIPAssociation(neutron.NeutronResource): FIXED_IP_ADDRESS: properties.Schema( properties.Schema.STRING, _('IP address to use if the port has multiple addresses.'), - update_allowed=True + update_allowed=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), } diff --git a/heat/engine/resources/openstack/neutron/loadbalancer.py b/heat/engine/resources/openstack/neutron/loadbalancer.py index 25d84a717d..30021f191c 100644 --- a/heat/engine/resources/openstack/neutron/loadbalancer.py +++ b/heat/engine/resources/openstack/neutron/loadbalancer.py @@ -278,7 +278,10 @@ class Pool(neutron.NeutronResource): ), VIP_ADDRESS: properties.Schema( properties.Schema.STRING, - _('IP address of the vip.') + _('IP address of the vip.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), VIP_CONNECTION_LIMIT: properties.Schema( properties.Schema.INTEGER, @@ -547,7 +550,10 @@ class PoolMember(neutron.NeutronResource): ADDRESS: properties.Schema( properties.Schema.STRING, _('IP address of the pool member on the pool network.'), - required=True + required=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), PROTOCOL_PORT: properties.Schema( properties.Schema.INTEGER, diff --git a/heat/engine/resources/openstack/neutron/port.py b/heat/engine/resources/openstack/neutron/port.py index 486351d332..c24f42861b 100644 --- a/heat/engine/resources/openstack/neutron/port.py +++ b/heat/engine/resources/openstack/neutron/port.py @@ -132,7 +132,10 @@ class Port(neutron.NeutronResource): ), FIXED_IP_IP_ADDRESS: properties.Schema( properties.Schema.STRING, - _('IP address desired in the subnet for this port.') + _('IP address desired in the subnet for this port.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), }, ), @@ -166,7 +169,10 @@ class Port(neutron.NeutronResource): ALLOWED_ADDRESS_PAIR_IP_ADDRESS: properties.Schema( properties.Schema.STRING, _('IP address to allow through this port.'), - required=True + required=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), }, ) diff --git a/heat/engine/resources/openstack/neutron/subnet.py b/heat/engine/resources/openstack/neutron/subnet.py index 37da107265..8b281f6080 100644 --- a/heat/engine/resources/openstack/neutron/subnet.py +++ b/heat/engine/resources/openstack/neutron/subnet.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import netutils + from heat.common import exception from heat.common.i18n import _ from heat.engine import attributes @@ -130,11 +132,17 @@ class Subnet(neutron.NeutronResource): schema={ ALLOCATION_POOL_START: properties.Schema( properties.Schema.STRING, - required=True + required=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), ALLOCATION_POOL_END: properties.Schema( properties.Schema.STRING, - required=True + required=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), }, ) @@ -155,7 +163,10 @@ class Subnet(neutron.NeutronResource): ), ROUTE_NEXTHOP: properties.Schema( properties.Schema.STRING, - required=True + required=True, + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), }, ), @@ -247,6 +258,13 @@ class Subnet(neutron.NeutronResource): 'they must be equal.') raise exception.StackValidationFailed(message=msg) + gateway_ip = self.properties.get(self.GATEWAY_IP) + if (gateway_ip and gateway_ip not in ['~', ''] and + not netutils.is_valid_ip(gateway_ip)): + msg = (_('Gateway IP address "%(gateway)" is in ' + 'invalid format.'), gateway_ip) + raise exception.StackValidationFailed(message=msg) + def handle_create(self): props = self.prepare_properties( self.properties, diff --git a/heat/engine/resources/openstack/nova/server.py b/heat/engine/resources/openstack/nova/server.py index bf6a5deaee..3862bca4e4 100644 --- a/heat/engine/resources/openstack/nova/server.py +++ b/heat/engine/resources/openstack/nova/server.py @@ -339,7 +339,10 @@ class Server(stack_user.StackUser): NETWORK_FIXED_IP: properties.Schema( properties.Schema.STRING, _('Fixed IP address to specify for the port ' - 'created on the requested network.') + 'created on the requested network.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), NETWORK_PORT: properties.Schema( properties.Schema.STRING, diff --git a/heat/engine/resources/openstack/trove/os_database.py b/heat/engine/resources/openstack/trove/os_database.py index 00063258d7..05663991c4 100644 --- a/heat/engine/resources/openstack/trove/os_database.py +++ b/heat/engine/resources/openstack/trove/os_database.py @@ -149,7 +149,10 @@ class OSDBInstance(resource.Resource): ), V4_FIXED_IP: properties.Schema( properties.Schema.STRING, - _('Fixed IPv4 address for this NIC.') + _('Fixed IPv4 address for this NIC.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ] ), }, ), diff --git a/heat/tests/neutron/test_neutron_client.py b/heat/tests/neutron/test_neutron_client.py index f311b47abe..623cf4c47f 100644 --- a/heat/tests/neutron/test_neutron_client.py +++ b/heat/tests/neutron/test_neutron_client.py @@ -182,3 +182,50 @@ class NeutronConstraintsValidate(common.HeatTestCase): self.assertFalse(constraint.validate("bar", ctx)) mock_find.assert_has_calls([mock.call(nc, self.resource_type, 'foo'), mock.call(nc, self.resource_type, 'bar')]) + + +class TestIPConstraint(common.HeatTestCase): + + def setUp(self): + super(TestIPConstraint, self).setUp() + self.constraint = neutron.IPConstraint() + + def test_validate_ipv4_format(self): + validate_format = [ + '1.1.1.1', + '1.0.1.1', + '255.255.255.255' + ] + for ip in validate_format: + self.assertTrue(self.constraint.validate(ip, None)) + + def test_invalidate_ipv4_format(self): + invalidate_format = [ + '1.1.1.', + '1.1.1.256', + 'invalidate format', + '1.a.1.1' + ] + for ip in invalidate_format: + self.assertFalse(self.constraint.validate(ip, None)) + + def test_validate_ipv6_format(self): + validate_format = [ + '2002:2002::20c:29ff:fe7d:811a', + '::1', + '2002::', + '2002::1', + ] + for ip in validate_format: + self.assertTrue(self.constraint.validate(ip, None)) + + def test_invalidate_ipv6_format(self): + invalidate_format = [ + '2002::2001::1', + '2002::g', + 'invalidate format', + '2001::0::', + '20c:29ff:fe7d:811a' + ] + for ip in invalidate_format: + self.assertFalse(self.constraint.validate(ip, None)) diff --git a/setup.cfg b/setup.cfg index 38c824a3ec..3dcb4aa9cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,6 +72,7 @@ heat.constraints = cinder.vtype = heat.engine.clients.os.cinder:VolumeTypeConstraint sahara.image = heat.engine.clients.os.sahara:ImageConstraint trove.flavor = heat.engine.clients.os.trove:FlavorConstraint + ip_addr = heat.engine.clients.os.neutron:IPConstraint heat.stack_lifecycle_plugins =