diff --git a/tripleo_ansible/ansible_plugins/module_utils/network_data_v2.py b/tripleo_ansible/ansible_plugins/module_utils/network_data_v2.py new file mode 100644 index 000000000..df37a1455 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/module_utils/network_data_v2.py @@ -0,0 +1,405 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2018 OpenStack Foundation +# 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. + +import collections +import ipaddress +import jsonschema +import yaml + +DOMAIN_NAME_REGEX = (r'^(?=^.{1,255}$)(?!.*\.\..*)(.{1,63}\.)' + r'+(.{0,63}\.?)|(?!\.)(?!.*\.\..*)(^.{1,63}$)' + r'|(^\.$)$') +NET_DATA_V2_SCHEMA = ''' +--- +$schema: http://json-schema.org/draft-04/schema + +definitions: + domain_name_string: + type: string + pattern: {domain_name_regex} + ipv4_allocation_pool: + type: object + properties: + start: + type: string + ip_address_version: 4 + end: + type: string + ip_address_version: 4 + additionalProperties: False + uniqueItems: true + required: + - start + - end + ipv4_route: + type: object + properties: + destination: + type: string + ip_subnet_version: 4 + nexthop: + type: string + ip_address_version: 4 + additionalProperties: False + uniqueItems: true + required: + - destination + - nexthop + ipv6_allocation_pool: + type: object + properties: + start: + type: string + ip_address_version: 6 + end: + type: string + ip_address_version: 6 + additionalProperties: False + uniqueItems: true + required: + - start + - end + ipv6_route: + type: object + properties: + destination: + type: string + ip_subnet_version: 6 + nexthop: + type: string + ip_address_version: 6 + additionalProperties: False + uniqueItems: true + required: + - destination + - nexthop + + ipv4_subnet: + type: object + properties: + ip_subnet: + type: string + ip_subnet_version: 4 + gateway_ip: + type: string + ip_address_version: 4 + allocation_pools: + type: array + items: + $ref: "#/definitions/ipv4_allocation_pool" + enable_dhcp: + type: boolean + routes: + type: array + items: + $ref: "#/definitions/ipv4_route" + vlan: + type: integer + minimum: 1 + maximum: 4096 + physical_network: + type: string + network_type: + enum: + - flat + - vlan + segmentation_id: + type: integer + minimum: 1 + maximum: 4096 + additionalProperties: False + required: + - ip_subnet + + ipv6_subnet: + type: object + properties: + ipv6_subnet: + type: string + ip_subnet_version: 6 + gateway_ipv6: + type: string + ip_address_version: 6 + ipv6_allocation_pools: + type: array + items: + $ref: "#/definitions/ipv6_allocation_pool" + routes_ipv6: + type: array + items: + $ref: "#/definitions/ipv6_route" + ipv6_address_mode: + enum: + - null + - dhcpv6-stateful + - dhcpv6-stateless + ipv6_ra_mode: + enum: + - null + - dhcpv6-stateful + - dhcpv6-stateless + enable_dhcp: + type: boolean + vlan: + type: integer + minimum: 1 + maximum: 4096 + physical_network: + type: string + network_type: + type: string + enum: + - flat + - vlan + segmentation_id: + type: integer + minimum: 1 + maximum: 4096 + additionalProperties: False + required: + - ipv6_subnet + + dual_subnet: + type: object + properties: + ip_subnet: + type: string + ip_subnet_version: 4 + gateway_ip: + type: string + ip_address_version: 4 + allocation_pools: + type: array + items: + $ref: "#/definitions/ipv4_allocation_pool" + routes: + type: array + items: + $ref: "#/definitions/ipv4_route" + ipv6_subnet: + type: string + ip_subnet_version: 6 + gateway_ipv6: + type: string + ip_address_version: 6 + ipv6_allocation_pools: + type: array + items: + $ref: "#/definitions/ipv6_allocation_pool" + routes_ipv6: + type: array + items: + $ref: "#/definitions/ipv6_route" + ipv6_address_mode: + enum: + - null + - dhcpv6-stateful + - dhcpv6-stateless + ipv6_ra_mode: + enum: + - null + - dhcpv6-stateful + - dhcpv6-stateless + enable_dhcp: + type: boolean + vlan: + type: integer + minimum: 1 + maximum: 4096 + physical_network: + type: string + network_type: + type: string + enum: + - flat + - vlan + segmentation_id: + type: integer + minimum: 1 + maximum: 4096 + additionalProperties: False + required: + - ip_subnet + - ipv6_subnet + +type: object +properties: + name: + type: string + name_lower: + type: string + admin_state_up: + type: boolean + dns_domain: + $ref: "#/definitions/domain_name_string" + mtu: + type: integer + minimum: 1000 + maximum: 65536 + shared: + type: boolean + service_net_map_replace: + type: string + ipv6: + type: boolean + vip: + type: boolean + subnets: + type: object + additionalProperties: + oneOf: + - $ref: "#/definitions/ipv4_subnet" + - $ref: "#/definitions/ipv6_subnet" + - $ref: "#/definitions/dual_subnet" +additionalProperties: False +required: +- name +- subnets +'''.format(domain_name_regex=DOMAIN_NAME_REGEX) + + +def _get_detailed_errors(error, depth, absolute_schema_path, absolute_schema, + filter_errors=True): + """Returns a list of error messages from all subschema validations. + + Recurses the error tree and adds one message per sub error. That list can + get long, because jsonschema also tests the hypothesis that the provided + network element type is wrong (e.g. "ovs_bridge" instead of "ovs_bond"). + Setting `filter_errors=True` assumes the type, if specified, is correct and + therefore produces a much shorter list of more relevant results. + """ + + if not error.context: + return [] + + sub_errors = error.context + if filter_errors: + if (absolute_schema_path[-1] in ['oneOf', 'anyOf'] + and isinstance(error.instance, collections.Mapping) + and 'type' in error.instance): + found, index = _find_type_in_schema_list( + error.validator_value, error.instance['type']) + if found: + sub_errors = [i for i in sub_errors if ( + i.schema_path[0] == index)] + + details = [] + sub_errors = sorted(sub_errors, key=lambda e: e.schema_path) + for sub_error in sub_errors: + schema_path = collections.deque(absolute_schema_path) + schema_path.extend(sub_error.schema_path) + details.append("{} {}: {}".format( + '-' * depth, + _pretty_print_schema_path(schema_path, absolute_schema), + sub_error.message) + ) + details.extend(_get_detailed_errors( + sub_error, depth + 1, schema_path, absolute_schema, + filter_errors)) + return details + + +def _find_type_in_schema_list(schemas, type): + """Finds an object of a given type in an anyOf/oneOf array. + + Returns a tuple (`found`, `index`), where `found` indicates whether + on object of type `type` was found in the `schemas` array. + If so, `index` contains the object's position in the array. + """ + for index, schema in enumerate(schemas): + if not isinstance(schema, collections.Mapping): + continue + if '$ref' in schema and schema['$ref'].split('/')[-1] == type: + return True, index + if ('properties' in schema and 'type' in schema['properties'] + and schema['properties']['type'] == type): + return True, index + return False, 0 + + +def _pretty_print_schema_path(absolute_schema_path, absolute_schema): + """Returns a representation of the schema path that's easier to read. + + For example: + >>> _pretty_print_schema_path("items/oneOf/0/properties/use_dhcp/oneOf/2") + "items/oneOf/interface/use_dhcp/oneOf/param" + """ + + pretty_path = [] + current_path = [] + current_schema = absolute_schema + for item in absolute_schema_path: + if item not in ["properties"]: + pretty_path.append(item) + current_path.append(item) + current_schema = current_schema[item] + if (isinstance(current_schema, collections.Mapping) + and '$ref' in current_schema): + if (isinstance(pretty_path[-1], int) and pretty_path[-2] + in ['oneOf', 'anyOf']): + pretty_path[-1] = current_schema['$ref'].split('/')[-1] + current_path = current_schema['$ref'].split('/') + current_schema = absolute_schema + for i in current_path[1:]: + current_schema = current_schema[i] + return '/'.join([str(x) for x in pretty_path]) + + +def validate_json_schema(net_data): + + def ip_subnet_version_validator(validator, ip_version, instance, schema): + msg = '{} does not appear to be an IPv{} subnet'.format( + instance, ip_version) + try: + if not ipaddress.ip_network(instance).version == ip_version: + yield jsonschema.ValidationError(msg) + except ValueError: + yield jsonschema.ValidationError(msg) + + def ip_address_version_validator(validator, ip_version, instance, schema): + msg = '{} does not appear to be an IPv{} address'.format( + instance, ip_version) + try: + if not ipaddress.ip_address(instance).version == ip_version: + yield jsonschema.ValidationError(msg) + except ValueError: + yield jsonschema.ValidationError(msg) + + schema = yaml.safe_load(NET_DATA_V2_SCHEMA) + net_data_validator = jsonschema.validators.extend( + jsonschema.Draft4Validator, + validators={'ip_subnet_version': ip_subnet_version_validator, + 'ip_address_version': ip_address_version_validator}) + validator = net_data_validator(schema) + errors = validator.iter_errors(instance=net_data) + + error_messages = [] + for error in errors: + details = _get_detailed_errors(error, 1, error.schema_path, schema) + + config_path = '/'.join([str(x) for x in error.path]) + if details: + error_messages.append( + "Failed schema validation at {}:\n {}\n" + " Sub-schemas tested and not matching:\n {}".format( + config_path, error.message, '\n '.join(details))) + else: + error_messages.append( + "Failed schema validation at {}:\n {}".format( + config_path, error.message)) + + return error_messages diff --git a/tripleo_ansible/tests/modules/test_network_data_v2.py b/tripleo_ansible/tests/modules/test_network_data_v2.py new file mode 100644 index 000000000..b5ac460a0 --- /dev/null +++ b/tripleo_ansible/tests/modules/test_network_data_v2.py @@ -0,0 +1,411 @@ +# Copyright 2019 Red Hat, 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. + +import yaml +import copy + +from tripleo_ansible.tests import base as tests_base + +from tripleo_ansible.ansible_plugins.module_utils import network_data_v2 + + +NET_DATA = yaml.safe_load(''' +--- +name: Storage +name_lower: storage +admin_state_up: false +dns_domain: storage.localdomain. +mtu: 1442 +shared: false +service_net_map_replace: storage +ipv6: true +vip: true +subnets: + subnet01: + ip_subnet: 172.18.1.0/24 + gateway_ip: 172.18.1.254 + allocation_pools: + - start: 172.18.1.1 + end: 172.18.1.250 + routes: + - destination: 172.18.0.0/24 + nexthop: 172.18.1.254 + ipv6_subnet: 2001:db8:a::/64 + gateway_ipv6: 2001:db8:a::1 + ipv6_allocation_pools: + - start: 2001:db8:a::0010 + end: 2001:db8:a::fff9 + routes_ipv6: + - destination: 2001:db8:b::/64 + nexthop: 2001:db8:a::1 + ipv6_address_mode: null + ipv6_ra_mode: null + enable_dhcp: false + physical_network: storage_subnet01 + network_type: flat + segmentation_id: 21 + vlan: 21 + subnet02: + ip_subnet: 172.18.0.0/24 + gateway_ip: 172.18.0.254 + allocation_pools: + - start: 172.18.0.10 + end: 172.18.0.250 + routes: + - destination: 172.18.1.0/24 + nexthop: 172.18.0.254 + ipv6_subnet: 2001:db8:b::/64 + gateway_ipv6: 2001:db8:b::1 + ipv6_allocation_pools: + - start: 2001:db8:b::0010 + end: 2001:db8:b::fff9 + routes_ipv6: + - destination: 2001:db8:a::/64 + nexthop: 2001:db8:b::1 + ipv6_address_mode: null + ipv6_ra_mode: null + enable_dhcp: false + physical_network: storage_subnet01 + network_type: flat + segmentation_id: 21 + vlan: 20 +''') + +IPV4_SUBNET_KEYS = {'ip_subnet', 'allocation_pools', 'routes', 'gateway_ip'} +IPV6_SUBNET_KEYS = {'ipv6_subnet', 'ipv6_allocation_pools', 'routes_ipv6', + 'gateway_ipv6', 'ipv6_address_mode', 'ipv6_ra_mode'} + + +class TestNetworkDataV2(tests_base.TestCase): + + def test_validator_ok(self): + ipv4_only = copy.deepcopy(NET_DATA) + ipv6_only = copy.deepcopy(NET_DATA) + ipv4_only.pop('ipv6') + for name, subnet in ipv4_only['subnets'].items(): + [subnet.pop(k) for k in IPV6_SUBNET_KEYS] + for name, subnet in ipv6_only['subnets'].items(): + [subnet.pop(k) for k in IPV4_SUBNET_KEYS] + + error_messages = network_data_v2.validate_json_schema(NET_DATA) + self.assertEqual([], error_messages) + error_messages = network_data_v2.validate_json_schema(ipv4_only) + self.assertEqual([], error_messages) + error_messages = network_data_v2.validate_json_schema(ipv6_only) + self.assertEqual([], error_messages) + + def test_validator_fail(self): + dual = copy.deepcopy(NET_DATA) + dual.pop('name') # Required + dual['mtu'] = 400 # too low + dual['ipv6'] = 'not_bool' + dual['vip'] = 'not_bool' + dual['admin_state_up'] = 'not_bool' + dual['shared'] = 'not_bool' + dual['invalid_key'] = 'foo' + s02 = dual['subnets']['subnet02'] + s02['ip_subnet'] = 'invalid' + s02['gateway_ip'] = '2001:db8:a::1' # Wrong ip version + s02['allocation_pools'][0]['foo'] = 'foo' # Invalid key + s02['allocation_pools'][0]['start'] = '2001:db8:a::1' + s02['routes'][0]['foo'] = 'foo' # Invalid key + s02['routes'][0]['nexthop'] = '172222.18.1.254' + s02['routes'][0]['destination'] = '172.18.0.0/99' # netmask error + s02['enable_dhcp'] = 'not_a_bool' + s02['physical_network'] = dict() # Invalid, should be string + s02['network_type'] = 'invalid' + s02['vlan'] = 'not_an_int' + s02['ipv6_subnet'] = 'invalid' + s02['gateway_ipv6'] = '172.18.1.254' # Wrong ip version + s02['ipv6_allocation_pools'][0]['v6_invalid_key'] = 'foo' + s02['ipv6_allocation_pools'][0]['end'] = '172.18.1.20' + s02['routes_ipv6'][0]['v6_invalid_key'] = 'foo' + s02['routes_ipv6'][0]['destination'] = '2001:XXX8:X::/64' + s02['ipv6_address_mode'] = 'invalid' + s02['ipv6_ra_mode'] = 'invalid' + + ipv4_only = copy.deepcopy(dual) + ipv6_only = copy.deepcopy(dual) + for name, subnet in ipv4_only['subnets'].items(): + [subnet.pop(k) for k in IPV6_SUBNET_KEYS] + for name, subnet in ipv6_only['subnets'].items(): + [subnet.pop(k) for k in IPV4_SUBNET_KEYS] + ipv4_only['subnets']['subnet01'].pop('ip_subnet') # Required + ipv6_only['subnets']['subnet01'].pop('ipv6_subnet') # Required + + error_messages_dual = network_data_v2.validate_json_schema(dual) + error_messages_dual = '\n'.join(error_messages_dual) + error_messages_ipv4 = network_data_v2.validate_json_schema(ipv4_only) + error_messages_ipv4 = '\n'.join(error_messages_ipv4) + error_messages_ipv6 = network_data_v2.validate_json_schema(ipv6_only) + error_messages_ipv6 = '\n'.join(error_messages_ipv6) + + self.assertRegex(error_messages_dual, + (r"Failed schema validation at admin_state_up:\n" + r" 'not_bool' is not of type 'boolean'\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at mtu:\n" + r" 400 is less than the minimum of 1000\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at shared:\n" + r" 'not_bool' is not of type 'boolean'\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at ipv6:\n" + r" 'not_bool' is not of type 'boolean'\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at vip:\n" + r" 'not_bool' is not of type 'boolean'\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at :\n" + r".*Additional properties are not allowed " + r"\('invalid_key' was unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"Failed schema validation at :\n" + r".*'name' is a required property")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/allocation_pools/items/additionalProperties: " + r"Additional properties are not allowed \('foo' was " + r"unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/allocation_pools/items/start/ip_address_version: " + r"2001:db8:a::1 does not appear to be an IPv4 " + r"address\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/enable_dhcp/type: 'not_a_bool' is not of type " + r"'boolean'\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/gateway_ip/ip_address_version: 2001:db8:a::1 " + r"does not appear to be an IPv4 address\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/gateway_ipv6/ip_address_version: 172.18.1.254 " + r"does not appear to be an IPv6 address\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ip_subnet/ip_subnet_version: invalid does not " + r"appear to be an IPv4 subnet\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ipv6_address_mode/enum: 'invalid' is not one of " + r"\[None, 'dhcpv6-stateful', 'dhcpv6-stateless'\]" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ipv6_allocation_pools/items" + r"/additionalProperties: Additional properties are " + r"not allowed \('v6_invalid_key' was unexpected\)" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ipv6_allocation_pools/items/end" + r"/ip_address_version: 172.18.1.20 does not appear " + r"to be an IPv6 address\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ipv6_ra_mode/enum: 'invalid' is not one of " + r"\[None, 'dhcpv6-stateful', 'dhcpv6-stateless'\]\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/ipv6_subnet/ip_subnet_version: invalid does not " + r"appear to be an IPv6 subnet\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/network_type/enum: 'invalid' is not one of " + r"\['flat', 'vlan'\]\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/physical_network/type: {} is not of type 'string'" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/routes/items/additionalProperties: Additional " + r"properties are not allowed \('foo' was unexpected\)" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/routes/items/destination/ip_subnet_version: " + r"172.18.0.0/99 does not appear to be an IPv4 subnet" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/routes/items/nexthop/ip_address_version: " + r"172222.18.1.254 does not appear to be an IPv4 " + r"address\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/routes_ipv6/items/additionalProperties: " + r"Additional properties are not allowed \(" + r"'v6_invalid_key' was unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/routes_ipv6/items/destination/ip_subnet_version: " + r"2001:XXX8:X::/64 does not appear to be an IPv6 " + r"subnet\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/dual_subnet" + r"/vlan/type: 'not_an_int' is not of type 'integer'" + r"\n")) + + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'routes'.* were unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'allocation_pools'.* were " + r"unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'ip_subnet'.* were unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'gateway_ip'.* were unexpected\)" + r"\n")) + + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'routes_ipv6'.* were unexpected\)" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'ipv6_allocation_pools'.* were " + r"unexpected\)\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'ipv6_subnet'.* were unexpected\)" + r"\n")) + self.assertRegex(error_messages_dual, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/additionalProperties: Additional properties are " + r"not allowed \(.*'gateway_ipv6'.* were unexpected\)" + r"\n")) + + self.assertRegex(error_messages_ipv4, + r"Failed schema validation at subnets/subnet01:\n") + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/required: 'ip_subnet' is a required property\n")) + self.assertRegex(error_messages_ipv4, + r"Failed schema validation at subnets/subnet02:\n") + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/allocation_pools/items/additionalProperties: " + r"Additional properties are not allowed \('foo' was " + r"unexpected\)\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/allocation_pools/items/start/ip_address_version: " + r"2001:db8:a::1 does not appear to be an IPv4 " + r"address\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet/" + r"enable_dhcp/type: \'not_a_bool\' is not of type " + r"\'boolean\'\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/gateway_ip/ip_address_version: 2001:db8:a::1 does " + r"not appear to be an IPv4 address\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/ip_subnet/ip_subnet_version: invalid does not " + r"appear to be an IPv4 subnet\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/network_type/enum: 'invalid' is not one of " + r"\['flat', 'vlan'\].*")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/physical_network/type: {} is not of type 'string'" + r"\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/routes/items/additionalProperties: Additional " + r"properties are not allowed \('foo' was " + r"unexpected\)\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/routes/items/destination/ip_subnet_version: " + r"172.18.0.0/99 does not appear to be an IPv4 subnet" + r"\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/routes/items/nexthop/ip_address_version: " + r"172222.18.1.254 does not appear to be an IPv4 " + r"address\n")) + self.assertRegex(error_messages_ipv4, + (r"- subnets/additionalProperties/oneOf/ipv4_subnet" + r"/vlan/type: 'not_an_int' is not of type 'integer'" + r"\n")) + + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/enable_dhcp/type: 'not_a_bool' is not of type " + r"'boolean'\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/gateway_ipv6/ip_address_version: 172.18.1.254 " + r"does not appear to be an IPv6 address\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/ipv6_address_mode/enum: 'invalid' is not one of " + r"\[None, 'dhcpv6-stateful', 'dhcpv6-stateless'\]" + r"\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/ipv6_allocation_pools/items" + r"/additionalProperties: Additional properties are " + r"not allowed \('v6_invalid_key' was unexpected\)" + r"\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/ipv6_allocation_pools/items/end" + r"/ip_address_version: 172.18.1.20 does not appear " + r"to be an IPv6 address\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/ipv6_subnet/ip_subnet_version: invalid does not " + r"appear to be an IPv6 subnet\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/network_type/enum: 'invalid' is not one of " + r"\['flat', 'vlan'\]\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/physical_network/type: {} is not of type 'string'" + r"\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/routes_ipv6/items/additionalProperties: " + r"Additional properties are not allowed " + r"\('v6_invalid_key' was unexpected\)\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/routes_ipv6/items/destination/ip_subnet_version: " + r"2001:XXX8:X::/64 does not appear to be an IPv6 " + r"subnet\n")) + self.assertRegex(error_messages_ipv6, + (r"- subnets/additionalProperties/oneOf/ipv6_subnet" + r"/vlan/type: 'not_an_int' is not of type 'integer'" + r"\n"))