module_utils - network_data_v2
Add's a module utils for network data v2. The utils module provides the schema validator for the network data v2 format. The json schema validator copies the validation errors processing code from os-net-config with just minor changes. Change-Id: Ia68e2536af04bbe5d4ed0561eb923e8778563a92
This commit is contained in:
parent
9f9fe8a60f
commit
292602e070
|
@ -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
|
|
@ -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"))
|
Loading…
Reference in New Issue