From 9ef9a7a92dc7af2a7d54affd68aaa37539296dd4 Mon Sep 17 00:00:00 2001 From: "Frank A. Zdarsky" Date: Fri, 20 Jan 2017 12:53:52 +0100 Subject: Add schema-based config data validation This patch adds a jsonschema for os-net-config's configuration data and a library function to validate configuration data based on this schema. Adding schema-based validation allows catching a larger class of errors (typos, missing required parameters, etc.) for all devices configurable through os-net-config. The validation is run in the os-net-config CLI after loading the config file. If the config file fails to validate, the current default is to just log a warning and try to continue. By providing the new CLI option '--exit-on-validation-errors', this can be changed to log an error and exist instead. This validation is meant to be reusable, for example for pre-deployment validation of network environments (see change Ic16ee0bc353c46f8fe512454176a07ee95347346). Packaging with os-net-config makes it easier to keep object model and schema in sync. Change-Id: Ie4a905863b2d46c88d9cd6c3afc50e7d0a877090 Signed-off-by: Frank A. Zdarsky --- os_net_config/cli.py | 19 + os_net_config/objects.py | 5 + os_net_config/schema.yaml | 1003 +++++++++++++++++++++++++++++++++ os_net_config/tests/test_cli.py | 43 +- os_net_config/tests/test_validator.py | 392 +++++++++++++ os_net_config/validator.py | 181 ++++++ requirements.txt | 1 + 7 files changed, 1632 insertions(+), 12 deletions(-) create mode 100644 os_net_config/schema.yaml create mode 100644 os_net_config/tests/test_validator.py create mode 100644 os_net_config/validator.py diff --git a/os_net_config/cli.py b/os_net_config/cli.py index 479b3a3..341dcdd 100644 --- a/os_net_config/cli.py +++ b/os_net_config/cli.py @@ -25,6 +25,7 @@ from os_net_config import impl_eni from os_net_config import impl_ifcfg from os_net_config import impl_iproute from os_net_config import objects +from os_net_config import validator from os_net_config import version @@ -55,6 +56,14 @@ def parse_opts(argv): """that files were modified.""" """Disabled by default.""", default=False) + + parser.add_argument( + '--exit-on-validation-errors', + action='store_true', + help="Exit with an error if configuration file validation fails. " + "Without this option, just log a warning and continue.", + default=False) + parser.add_argument( '-d', '--debug', dest="debug", @@ -181,6 +190,16 @@ def main(argv=sys.argv): for iface_json in iface_array: iface_json.update({'nic_mapping': iface_mapping}) iface_json.update({'persist_mapping': persist_mapping}) + + validation_errors = validator.validate_config(iface_array) + if validation_errors: + if opts.exit_on_validation_errors: + logger.error('\n'.join(validation_errors)) + return 1 + else: + logger.warning('\n'.join(validation_errors)) + + for iface_json in iface_array: obj = objects.object_from_json(iface_json) provider.add_object(obj) files_changed = provider.apply(cleanup=opts.cleanup, diff --git a/os_net_config/objects.py b/os_net_config/objects.py index 5fe6e49..a6d0c03 100644 --- a/os_net_config/objects.py +++ b/os_net_config/objects.py @@ -14,6 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +# +# NOTE: When making changes to the object model, remember to also update +# schema.yaml to reflect changes to the schema of config files! +# + import logging import netaddr from oslo_utils import strutils diff --git a/os_net_config/schema.yaml b/os_net_config/schema.yaml new file mode 100644 index 0000000..5060a34 --- /dev/null +++ b/os_net_config/schema.yaml @@ -0,0 +1,1003 @@ +--- +$schema: http://json-schema.org/draft-04/schema + +definitions: + # base types + param: + oneOf: + - type: object + properties: + get_param: + type: string + additionalProperties: False + - type: object + properties: + get_input: + type: string + additionalProperties: False + string_or_param: + oneOf: + - type: string + - $ref: "#/definitions/param" + int_or_param: + oneOf: + - type: integer + - $ref: "#/definitions/param" + bool_or_param: + oneOf: + - type: boolean + - # also accept strings of boolean values (like oslo_utils.strutils) + type: string + pattern: "(?i)^(t|true|on|y|yes|1|f|false|off|n|no|0)$" + - $ref: "#/definitions/param" + + # IP address and address+prefix types + ipv4_address_string: + type: string + pattern: "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}\ + (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + ipv6_address_string: + type: string + pattern: "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}\ + |([0-9a-fA-F]{1,4}:){1,7}:\ + |([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\ + |([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}\ + |([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}\ + |([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}\ + |([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}\ + |[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})\ + |:((:[0-9a-fA-F]{1,4}){1,7}|:))$" + ip_address_string: + oneOf: + - $ref: "#/definitions/ipv4_address_string" + - $ref: "#/definitions/ipv6_address_string" + ip_address_string_or_param: + oneOf: + - $ref: "#/definitions/ip_address_string" + - $ref: "#/definitions/param" + list_of_ip_address_string_or_param: + oneOf: + - type: array + items: + $ref: "#/definitions/ip_address_string_or_param" + minItems: 1 + - $ref: "#/definitions/param" + + ipv4_cidr_string: + type: string + pattern: "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}\ + (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\ + /(3[0-2]|[1-2][0-9]|[0-9])$" + ipv6_cidr_string: + type: string + pattern: "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}\ + |([0-9a-fA-F]{1,4}:){1,7}:\ + |([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\ + |([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}\ + |([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}\ + |([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}\ + |([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}\ + |[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})\ + |:((:[0-9a-fA-F]{1,4}){1,7}|:))\ + /(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9])$" + ip_cidr_string: + oneOf: + - $ref: "#/definitions/ipv4_cidr_string" + - $ref: "#/definitions/ipv6_cidr_string" + ip_cidr_string_or_param: + oneOf: + - $ref: "#/definitions/ip_cidr_string" + - $ref: "#/definitions/param" + - type: object + properties: + list_join: + type: array + items: + - enum: ["/"] + - type: array + items: + - $ref: "#/definitions/ip_address_string_or_param" + - $ref: "#/definitions/int_or_param" + required: + - list_join + additionalProperties: False + + # os-net-config derived types + address: + type: object + properties: + ip_netmask: + $ref: "#/definitions/ip_cidr_string_or_param" + required: + - ip_netmask + additionalProperties: False + list_of_address: + type: array + items: + $ref: "#/definitions/address" + minItems: 1 + + route: + type: object + properties: + next_hop: + $ref: "#/definitions/ip_address_string_or_param" + ip_netmask: + $ref: "#/definitions/ip_cidr_string_or_param" + default: + $ref: "#/definitions/bool_or_param" + route_options: + $ref: "#/definitions/string_or_param" + required: + - next_hop + additionalProperties: False + list_of_route: + type: array + items: + $ref: "#/definitions/route" + minItems: 1 + + nic_mapping: + type: ["object", "null"] + + bonding_options: + type: string + + ovs_options_string: + type: string + pattern: "^((?:[a-zA-Z][a-zA-Z0-9: _-]*)=(?:[a-zA-Z0-9._-]+)[ ]*)+$" + ovs_options_string_or_param: + oneOf: + - $ref: "#/definitions/ovs_options_string" + - $ref: "#/definitions/param" + ovs_single_option_string: + type: string + pattern: "^([a-zA-Z][a-zA-Z0-9: _-]*)=([a-zA-Z0-9._-]+)$" + ovs_options_list: + type: array + items: + $ref: "#/definitions/ovs_single_option_string" + minItems: 1 + ovs_options_list_or_param: + oneOf: + - $ref: "#/definitions/ovs_options_list" + - $ref: "#/definitions/param" + ovs_fail_mode: + enum: ["standalone", "secure"] + ovs_fail_mode_or_param: + oneOf: + - $ref: "#/definitions/ovs_fail_mode" + - $ref: "#/definitions/param" + ovs_extra_string: + type: string + ovs_extra: + oneOf: + - $ref: "#/definitions/ovs_extra_string" + - type: array + items: + $ref: "#/definitions/ovs_extra_string" + minItems: 1 + ovs_extra_or_param: + oneOf: + - $ref: "#/definitions/ovs_extra" + - $ref: "#/definitions/param" + ovs_tunnel_type: + enum: ["vxlan", "gre"] + ovs_tunnel_type_or_param: + oneOf: + - $ref: "#/definitions/ovs_tunnel_type" + - $ref: "#/definitions/param" + + # os-net-config device types + interface: + type: object + properties: + type: + enum: ["interface"] + name: + $ref: "#/definitions/string_or_param" + primary: + $ref: "#/definitions/bool_or_param" + ethtool_opts: + $ref: "#/definitions/string_or_param" + hotplug: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + additionalProperties: False + + vlan: + type: object + properties: + type: + enum: ["vlan"] + vlan_id: + $ref: "#/definitions/int_or_param" + device: + $ref: "#/definitions/string_or_param" + primary: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - vlan_id + additionalProperties: False + + ovs_bridge: + type: object + properties: + type: + enum: ["ovs_bridge"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + - $ref: "#/definitions/linux_bond" + - $ref: "#/definitions/ovs_bond" + - $ref: "#/definitions/ovs_tunnel" + - $ref: "#/definitions/ovs_patch_port" + ovs_options: + $ref: "#/definitions/ovs_options_string_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + ovs_fail_mode: + $ref: "#/definitions/ovs_fail_mode_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + ovs_user_bridge: + type: object + properties: + type: + enum: ["ovs_user_bridge"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + - $ref: "#/definitions/ovs_bond" + - $ref: "#/definitions/ovs_patch_port" + - $ref: "#/definitions/ovs_tunnel" + - $ref: "#/definitions/ovs_dpdk_bond" + - $ref: "#/definitions/ovs_dpdk_port" + ovs_options: + $ref: "#/definitions/ovs_options_string_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + ovs_fail_mode: + $ref: "#/definitions/ovs_fail_mode_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + ovs_bond: + type: object + properties: + type: + enum: ["ovs_bond"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + minItems: 1 + ovs_options: + $ref: "#/definitions/ovs_options_string_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + ovs_patch_port: + type: object + properties: + type: + enum: ["ovs_patch_port"] + name: + $ref: "#/definitions/string_or_param" + bridge_name: + $ref: "#/definitions/string_or_param" + peer: + $ref: "#/definitions/string_or_param" + primary: + $ref: "#/definitions/bool_or_param" + ovs_options: + $ref: "#/definitions/ovs_options_list_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - bridge_name + - peer + additionalProperties: False + + ovs_tunnel: + type: object + properties: + type: + enum: ["ovs_tunnel"] + name: + $ref: "#/definitions/string_or_param" + tunnel_type: + $ref: "#/definitions/ovs_tunnel_type_or_param" + primary: + $ref: "#/definitions/bool_or_param" + ovs_options: + $ref: "#/definitions/ovs_options_list_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - tunnel_type + additionalProperties: False + + ovs_dpdk_bond: + type: object + properties: + type: + enum: ["ovs_dpdk_bond"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + - $ref: "#/definitions/ovs_dpdk_port" + ovs_options: + $ref: "#/definitions/ovs_options_string_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + rx_queue: + $ref: "#/definitions/int_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + ovs_dpdk_port: + type: object + properties: + type: + enum: ["ovs_dpdk_port"] + name: + $ref: "#/definitions/string_or_param" + driver: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + - $ref: "#/definitions/interface" + minItems: 1 + maxItems: 1 + primary: + $ref: "#/definitions/bool_or_param" + ovs_options: + $ref: "#/definitions/ovs_options_string_or_param" + ovs_extra: + $ref: "#/definitions/ovs_extra_or_param" + rx_queue: + $ref: "#/definitions/int_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + vpp_interface: + type: object + properties: + type: + enum: ["vpp_interface"] + name: + $ref: "#/definitions/string_or_param" + uio_driver: + $ref: "#/definitions/string_or_param" + options: + $ref: "#/definitions/string_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + additionalProperties: False + + linux_bridge: + type: object + properties: + type: + enum: ["linux_bridge"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + - $ref: "#/definitions/linux_bond" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + linux_bond: + type: object + properties: + type: + enum: ["linux_bond"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + bonding_options: + $ref: "#/definitions/bonding_options" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + linux_team: + type: object + properties: + type: + enum: ["team"] + name: + $ref: "#/definitions/string_or_param" + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + bonding_options: + $ref: "#/definitions/bonding_options" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - members + additionalProperties: False + + ivs_bridge: + type: object + properties: + type: + enum: ["ivs_bridge"] + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/ivs_interface" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - members + additionalProperties: False + + ivs_interface: + type: object + properties: + type: + enum: ["ivs_interface"] + name: + $ref: "#/definitions/string_or_param" + vlan_id: + $ref: "#/definitions/int_or_param" + primary: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - vlan_id + additionalProperties: False + + nfvswitch_bridge: + type: object + properties: + type: + enum: ["nfvswitch_bridge"] + members: + type: array + items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + - $ref: "#/definitions/nfvswitch_internal" + options: + $ref: "#/definitions/string_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - options + - members + additionalProperties: False + + nfvswitch_internal: + type: object + properties: + type: + enum: ["nfvswitch_internal"] + name: + $ref: "#/definitions/string_or_param" + vlan_id: + $ref: "#/definitions/int_or_param" + primary: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - vlan_id + additionalProperties: False + + ib_interface: + type: object + properties: + type: + enum: ["ib_interface"] + name: + $ref: "#/definitions/string_or_param" + primary: + $ref: "#/definitions/bool_or_param" + ethtool_opts: + $ref: "#/definitions/string_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + additionalProperties: False + +type: array +items: + oneOf: + - $ref: "#/definitions/interface" + - $ref: "#/definitions/vlan" + - $ref: "#/definitions/ovs_bridge" + - $ref: "#/definitions/ovs_user_bridge" + - $ref: "#/definitions/ovs_bond" + - $ref: "#/definitions/ovs_patch_port" + - $ref: "#/definitions/ovs_tunnel" + - $ref: "#/definitions/ovs_dpdk_bond" + - $ref: "#/definitions/ovs_dpdk_port" + - $ref: "#/definitions/linux_bridge" + - $ref: "#/definitions/linux_bond" + - $ref: "#/definitions/linux_team" + - $ref: "#/definitions/ivs_bridge" + - $ref: "#/definitions/ivs_interface" + - $ref: "#/definitions/nfvswitch_bridge" + - $ref: "#/definitions/nfvswitch_internal" + - $ref: "#/definitions/ib_interface" + - $ref: "#/definitions/vpp_interface" +minItems: 1 diff --git a/os_net_config/tests/test_cli.py b/os_net_config/tests/test_cli.py index 0626205..32f9395 100644 --- a/os_net_config/tests/test_cli.py +++ b/os_net_config/tests/test_cli.py @@ -52,9 +52,11 @@ class TestCli(base.TestCase): bond_yaml = os.path.join(SAMPLE_BASE, 'bond.yaml') bond_json = os.path.join(SAMPLE_BASE, 'bond.json') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % bond_yaml) self.assertEqual('', stderr) stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % bond_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=br-ctlplane', @@ -70,9 +72,11 @@ class TestCli(base.TestCase): ivs_yaml = os.path.join(SAMPLE_BASE, 'ivs.yaml') ivs_json = os.path.join(SAMPLE_BASE, 'ivs.json') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_yaml) self.assertEqual('', stderr) stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=nic2', @@ -87,11 +91,13 @@ class TestCli(base.TestCase): def test_bridge_noop_output(self): bridge_yaml = os.path.join(SAMPLE_BASE, 'bridge_dhcp.yaml') bridge_json = os.path.join(SAMPLE_BASE, 'bridge_dhcp.json') - stdout_yaml, stderr = self.run_cli('ARG0 --provider=eni --noop -c %s' % - bridge_yaml) + stdout_yaml, stderr = self.run_cli('ARG0 --provider=eni --noop ' + '--exit-on-validation-errors ' + '-c %s' % bridge_yaml) self.assertEqual('', stderr) - stdout_json, stderr = self.run_cli('ARG0 --provider=eni --noop -c %s' % - bridge_json) + stdout_json, stderr = self.run_cli('ARG0 --provider=eni --noop ' + '--exit-on-validation-errors ' + '-c %s' % bridge_json) self.assertEqual('', stderr) sanity_devices = ['iface br-ctlplane inet dhcp', 'iface em1', @@ -103,11 +109,13 @@ class TestCli(base.TestCase): def test_vlan_noop_output(self): vlan_yaml = os.path.join(SAMPLE_BASE, 'bridge_vlan.yaml') vlan_json = os.path.join(SAMPLE_BASE, 'bridge_vlan.json') - stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop -c %s' - % vlan_yaml) + stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % vlan_yaml) self.assertEqual('', stderr) - stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop -c %s' - % vlan_json) + stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % vlan_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=br-ctlplane', 'DEVICE=em1', @@ -120,11 +128,13 @@ class TestCli(base.TestCase): def test_interface_noop_output(self): interface_yaml = os.path.join(SAMPLE_BASE, 'interface.yaml') interface_json = os.path.join(SAMPLE_BASE, 'interface.json') - stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop -c %s' - % interface_yaml) + stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % interface_yaml) self.assertEqual('', stderr) - stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop -c %s' - % interface_json) + stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % interface_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=em1', 'BOOTPROTO=static', @@ -137,6 +147,7 @@ class TestCli(base.TestCase): for provider in ('ifcfg', 'eni'): bond_yaml = os.path.join(SAMPLE_BASE, 'bridge_dhcp.yaml') stdout_yaml, stderr = self.run_cli('ARG0 --provider=%s --noop ' + '--exit-on-validation-errors ' '--root-dir=/rootfs ' '-c %s' % (provider, bond_yaml)) self.assertEqual('', stderr) @@ -145,6 +156,7 @@ class TestCli(base.TestCase): def test_interface_noop_detailed_exit_codes(self): interface_yaml = os.path.join(SAMPLE_BASE, 'interface.yaml') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s --detailed-exit-codes' % interface_yaml, exitcodes=(2,)) @@ -162,6 +174,7 @@ class TestCli(base.TestCase): self.stubs.Set(impl_ifcfg, 'IfcfgNetConfig', TestImpl) stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s --detailed-exit-codes' % interface_yaml, exitcodes=(0,)) @@ -169,9 +182,11 @@ class TestCli(base.TestCase): ivs_yaml = os.path.join(SAMPLE_BASE, 'ovs_dpdk_bond.yaml') ivs_json = os.path.join(SAMPLE_BASE, 'ovs_dpdk_bond.json') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_yaml) self.assertEqual('', stderr) stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=br-link', @@ -186,9 +201,11 @@ class TestCli(base.TestCase): nfvswitch_yaml = os.path.join(SAMPLE_BASE, 'nfvswitch.yaml') nfvswitch_json = os.path.join(SAMPLE_BASE, 'nfvswitch.json') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % nfvswitch_yaml) self.assertEqual('', stderr) stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % nfvswitch_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=nic2', @@ -204,9 +221,11 @@ class TestCli(base.TestCase): ivs_yaml = os.path.join(SAMPLE_BASE, 'ovs_dpdk.yaml') ivs_json = os.path.join(SAMPLE_BASE, 'ovs_dpdk.json') stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_yaml) self.assertEqual('', stderr) stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' '-c %s' % ivs_json) self.assertEqual('', stderr) sanity_devices = ['DEVICE=br-link', diff --git a/os_net_config/tests/test_validator.py b/os_net_config/tests/test_validator.py new file mode 100644 index 0000000..7991d9f --- /dev/null +++ b/os_net_config/tests/test_validator.py @@ -0,0 +1,392 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017 Red Hat, 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. + +import glob +import jsonschema +import os.path +import yaml + +from os_net_config.tests import base +from os_net_config import validator + + +REALPATH = os.path.dirname(os.path.realpath(__file__)) +SAMPLE_BASE = os.path.join(REALPATH, '../../', 'etc', + 'os-net-config', 'samples') + + +class TestSchemaValidation(base.TestCase): + + def test_schema_is_valid(self): + schema = validator.get_os_net_config_schema() + jsonschema.Draft4Validator.check_schema(schema) + + def test__validate_config(self): + schema = {"type": "string"} + errors = validator._validate_config(42, "foo", schema, False) + self.assertEqual(len(errors), 1) + errors = validator._validate_config("42", "foo", schema, False) + self.assertEqual(len(errors), 0) + + def test_consistent_error_messages_type(self): + error = jsonschema.ValidationError( + "%r is not of type %r" % (u'name', u'string'), validator=u'type', + validator_value=u'string', instance=u'name') + msg = validator._get_consistent_error_message(error) + self.assertEqual(msg, "'name' is not of type 'string'") + + def test_consistent_error_messages_oneOf(self): + error = jsonschema.ValidationError( + "%r is not one of %r" % (u'type', [u'vlan', u'interface']), + validator=u'enum', validator_value=[u'vlan', u'interface'], + instance=u'type') + msg = validator._get_consistent_error_message(error) + self.assertEqual(msg, "'type' is not one of ['vlan','interface']") + + def test_consistent_error_messages_required(self): + error = jsonschema.ValidationError( + "%r is a required property" % u'name', validator=u'required') + msg = validator._get_consistent_error_message(error) + self.assertEqual(msg, "'name' is a required property") + error = jsonschema.ValidationError( + "u'name' is a required property", validator=u'required') + msg = validator._get_consistent_error_message(error) + self.assertEqual(msg, "'name' is a required property") + + def test_pretty_print_schema_path(self): + schema = validator.get_os_net_config_schema() + path = ['items', 'oneOf', 0, 'properties', 'name'] + path_string = validator._pretty_print_schema_path(path, schema) + self.assertEqual(path_string, "items/oneOf/interface/name") + + def test_find_type_in_list_of_references(self): + schemas = [ + {'$ref': '#/definitions/vlan'}, + {'properties': {'type': 'interface'}}, + None + ] + result = validator._find_type_in_schema_list(schemas, 'vlan') + self.assertEqual(result, (True, 0)) + result = validator._find_type_in_schema_list(schemas, 'interface') + self.assertEqual(result, (True, 1)) + result = validator._find_type_in_schema_list(schemas, 'ovs_bridge') + self.assertEqual(result, (False, 0)) + + def test_missing_required_property(self): + ifaces = [{"type": "interface"}] + errors = validator.validate_config(ifaces) + self.assertEqual(len(errors), 1) + self.assertIn("'name' is a required property", errors[0]) + + +class TestBaseTypes(base.TestCase): + + def test_param(self): + schema = validator.get_schema_for_defined_type("bool_or_param") + v = jsonschema.Draft4Validator(schema) + self.assertTrue(v.is_valid({"get_param": "foo"})) + self.assertTrue(v.is_valid({"get_input": "bar"})) + self.assertFalse(v.is_valid([])) + self.assertFalse(v.is_valid({})) + self.assertFalse(v.is_valid(None)) + self.assertFalse(v.is_valid("foo")) + + def test_bool_or_param(self): + schema = validator.get_schema_for_defined_type("bool_or_param") + v = jsonschema.Draft4Validator(schema) + self.assertTrue(v.is_valid(True)) + self.assertTrue(v.is_valid(False)) + self.assertTrue(v.is_valid("TRUE")) + self.assertTrue(v.is_valid("true")) + self.assertTrue(v.is_valid("yes")) + self.assertTrue(v.is_valid("1")) + self.assertTrue(v.is_valid("on")) + self.assertTrue(v.is_valid("false")) + self.assertTrue(v.is_valid("FALSE")) + self.assertTrue(v.is_valid("off")) + self.assertTrue(v.is_valid("no")) + self.assertTrue(v.is_valid("0")) + self.assertFalse(v.is_valid([])) + self.assertFalse(v.is_valid({})) + self.assertFalse(v.is_valid(None)) + self.assertFalse(v.is_valid("falsch")) + + def test_ip_address_string(self): + schema = validator.get_schema_for_defined_type("ip_address_string") + v = jsonschema.Draft4Validator(schema) + self.assertTrue(v.is_valid("0.0.0.0")) + self.assertTrue(v.is_valid("192.168.0.1")) + self.assertTrue(v.is_valid("::")) + self.assertTrue(v.is_valid("fe80::")) + self.assertTrue(v.is_valid("1:1:1::")) + self.assertFalse(v.is_valid("192.168.0.1/24")) + + def test_ip_cidr_string(self): + schema = validator.get_schema_for_defined_type("ip_cidr_string") + v = jsonschema.Draft4Validator(schema) + self.assertTrue(v.is_valid("0.0.0.0/0")) + self.assertTrue(v.is_valid("192.168.0.1/24")) + self.assertTrue(v.is_valid("::/0")) + self.assertTrue(v.is_valid("::1/128")) + self.assertTrue(v.is_valid("fe80::1/64")) + self.assertFalse(v.is_valid("193.168.0.1")) + + +class TestDerivedTypes(base.TestCase): + + def test_address(self): + schema = validator.get_schema_for_defined_type("address") + v = jsonschema.Draft4Validator(schema) + data = {"ip_netmask": "127.0.0.1/32"} + self.assertTrue(v.is_valid(data)) + data = {"ip_netmask": "127.0.0.1"} + self.assertFalse(v.is_valid(data)) + data = {"ip_netmask": None} + self.assertFalse(v.is_valid(data)) + data = {"ip_netmask": "127.0.0.1/32", "unkown_property": "value"} + self.assertFalse(v.is_valid(data)) + self.assertFalse(v.is_valid([])) + self.assertFalse(v.is_valid(None)) + + def test_list_of_address(self): + schema = validator.get_schema_for_defined_type("list_of_address") + v = jsonschema.Draft4Validator(schema) + data = {"ip_netmask": "127.0.0.1/32"} + self.assertTrue(v.is_valid([data])) + self.assertFalse(v.is_valid(data)) + self.assertFalse(v.is_valid([])) + self.assertFalse(v.is_valid(None)) + + def test_route(self): + schema = validator.get_schema_for_defined_type("route") + v = jsonschema.Draft4Validator(schema) + data = {"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", + "default": True, "route_options": "metric 10"} + self.assertTrue(v.is_valid(data)) + data["unkown_property"] = "value" + self.assertFalse(v.is_valid(data)) + self.assertFalse(v.is_valid({})) + self.assertFalse(v.is_valid([])) + self.assertFalse(v.is_valid(None)) + + +class TestDeviceTypes(base.TestCase): + + def test_interface(self): + schema = validator.get_schema_for_defined_type("interface") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "interface", + "name": "em1", + "use_dhcp": False, + "addresses": [{ + "ip_netmask": "192.0.2.1/24" + }], + "defroute": False, + "dhclient_args": "--foobar", + "dns_servers": ["1.2.3.4"], + "mtu": 1501, + "ethtool_opts": "speed 1000 duplex full", + "hotplug": True, + "routes": [{ + "next_hop": "192.0.2.1", + "ip_netmask": "192.0.2.1/24", + "route_options": "metric 10" + }] + } + self.assertTrue(v.is_valid(data)) + + def test_vlan(self): + schema = validator.get_schema_for_defined_type("vlan") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "vlan", + "vlan_id": 101, + "addresses": [{ + "ip_netmask": "192.0.2.1/24" + }] + } + self.assertTrue(v.is_valid(data)) + + def test_ovs_bridge(self): + schema = validator.get_schema_for_defined_type("ovs_bridge") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "ovs_bridge", + "name": "br-ctlplane", + "ovs_options": "lacp=active", + "ovs_extra": [ + "br-set-external-id br-ctlplane bridge-id br-ctlplane", + "set bridge {name} stp_enable=true" + ], + "ovs_fail_mode": "secure", + "members": [ + {"type": "interface", "name": "em1"} + ] + } + self.assertTrue(v.is_valid(data)) + + def test_ovs_bond(self): + schema = validator.get_schema_for_defined_type("ovs_bond") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "ovs_bond", + "name": "bond1", + "use_dhcp": "true", + "members": [ + {"type": "interface", "name": "em1"}, + {"type": "interface", "name": "em2"} + ] + } + self.assertTrue(v.is_valid(data)) + + def test_ovs_user_bridge(self): + schema = validator.get_schema_for_defined_type("ovs_user_bridge") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "ovs_user_bridge", + "name": "br-link", + "members": [{ + "type": "ovs_dpdk_bond", + "name": "dpdkbond0", + "mtu": 9000, + "rx_queue": 4, + "members": [{ + "type": "ovs_dpdk_port", + "name": "dpdk0", + "members": [{ + "type": "interface", + "name": "nic2" + }] + }, { + "type": "ovs_dpdk_port", + "name": "dpdk1", + "members": [{ + "type": "interface", + "name": "nic3" + }] + }] + }] + } + self.assertTrue(v.is_valid(data)) + + def test_ovs_patch_port(self): + schema = validator.get_schema_for_defined_type("ovs_patch_port") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "ovs_patch_port", + "name": "br_pub-patch", + "bridge_name": "br-ctlplane", + "peer": "br-ctlplane-patch" + } + self.assertTrue(v.is_valid(data)) + + def test_ovs_tunnel(self): + schema = validator.get_schema_for_defined_type("ovs_tunnel") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "ovs_tunnel", + "name": "tun0", + "tunnel_type": "vxlan", + "ovs_options": ["lacp=active"] + } + self.assertTrue(v.is_valid(data)) + + def test_vpp_interface(self): + schema = validator.get_schema_for_defined_type("vpp_interface") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "vpp_interface", + "name": "nic2", + "addresses": [ + {"ip_netmask": "192.0.2.1/24"} + ], + "uio_driver": "uio_pci_generic", + "options": "vlan-strip-offload off" + } + self.assertTrue(v.is_valid(data)) + + def test_linux_bridge(self): + schema = validator.get_schema_for_defined_type("linux_bridge") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "linux_bridge", + "name": "br-ctlplane", + "use_dhcp": True, + "members": [ + {"type": "interface", "name": "em1"} + ] + } + self.assertTrue(v.is_valid(data)) + + def test_linux_bond(self): + schema = validator.get_schema_for_defined_type("linux_bond") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "linux_bond", + "name": "bond1", + "use_dhcp": True, + "bonding_options": "mode=active-backup", + "members": [ + {"type": "interface", "name": "em1"}, + {"type": "interface", "name": "em2"} + ] + } + self.assertTrue(v.is_valid(data)) + + def test_nfvswitch_bridge(self): + schema = validator.get_schema_for_defined_type("nfvswitch_bridge") + v = jsonschema.Draft4Validator(schema) + data = { + "type": "nfvswitch_bridge", + "options": "-c 2,3,4,5", + "members": [{ + "type": "nfvswitch_internal", + "name": "api", + "addresses": [ + {"ip_netmask": "172.16.2.7/24"} + ], + "vlan_id": 201 + }, { + "type": "nfvswitch_internal", + "name": "storage", + "addresses": [ + {"ip_netmask": "172.16.1.6/24"} + ], + "vlan_id": 202 + }] + } + self.assertTrue(v.is_valid(data)) + + +class TestSampleFiles(base.TestCase): + + def test_sample_files(self): + sample_files = (glob.glob(os.path.join(SAMPLE_BASE, '*.json')) + + glob.glob(os.path.join(SAMPLE_BASE, '*.yaml'))) + for sample_file in sample_files: + with open(sample_file, 'r') as f: + try: + config = yaml.load(f.read()).get("network_config") + except Exception: + continue + if not config: + continue + errors = validator.validate_config(config, sample_file) + if os.path.basename(sample_file).startswith("invalid_"): + self.assertTrue(errors) + else: + self.assertFalse(errors) diff --git a/os_net_config/validator.py b/os_net_config/validator.py new file mode 100644 index 0000000..41cea07 --- /dev/null +++ b/os_net_config/validator.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017 Red Hat, 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. + +import collections +import copy +import jsonschema +import pkg_resources +import yaml + + +def get_os_net_config_schema(): + """Returns the schema for os_net_config's config files.""" + schema_string = pkg_resources.resource_string(__name__, "schema.yaml") + return yaml.load(schema_string) + + +def get_schema_for_defined_type(defined_type): + """Returns the schema for a given defined type of the full schema.""" + full_schema = get_os_net_config_schema() + type_schema = copy.deepcopy(full_schema["definitions"][defined_type]) + type_schema["$schema"] = full_schema["$schema"] + type_schema["definitions"] = full_schema["definitions"] + return type_schema + + +def validate_config(config, config_name="Config file"): + """Validates a list of interface/bridge configurations against the schema. + + If validation fails, returns a list of validation error message strings. + If validation succeeds, returns an empty list. + `config_name` can be used to prefix errors with a more specific name. + """ + return _validate_config(config, config_name, + get_os_net_config_schema(), True) + + +def _validate_config(config, config_name, schema, filter_errors): + error_messages = [] + validator = jsonschema.Draft4Validator(schema) + v_errors = validator.iter_errors(config) + v_errors = sorted(v_errors, key=lambda e: e.path) + for v_error in v_errors: + error_message = _get_consistent_error_message(v_error) + details = _get_detailed_errors(v_error, 1, v_error.schema_path, + schema, filter_errors=filter_errors) + + config_path = '/'.join([str(x) for x in v_error.path]) + if details: + error_messages.append( + "{} failed schema validation at network_config/{}:\n" + " {}\n" + " Sub-schemas tested and not matching:\n" + " {}" + .format(config_name, config_path, error_message, + '\n '.join(details))) + else: + error_messages.append( + "{} failed schema validation at network_config/{}:\n" + " {}" + .format(config_name, config_path, error_message)) + return error_messages + + +def _get_consistent_error_message(error): + """Returns error messages consistent across Python 2 and 3. + + jsonschema uses repr() to print its error messages, which means strings + will render as "u'...'" in Python 2 and "'...'" in Python 3, making + testing for error messages unnecessarily difficult. + """ + + if error.validator == 'type': + return "'{}' is not of type '{}'".format( + error.instance, error.validator_value) + elif error.validator == 'enum': + return "'{}' is not one of ['{}']".format( + error.instance, "','".join(error.validator_value)) + elif error.validator == 'required': + if error.message[0:2] == "u'": + return error.message[1:] + return error.message + + +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), + _get_consistent_error_message(sub_error))) + 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_to_find): + """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_to_find` 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_to_find): + return (True, index) + if ('properties' in schema and 'type' in schema['properties'] and + schema['properties']['type'] == type_to_find): + 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]) diff --git a/requirements.txt b/requirements.txt index 36544e1..e6e9b32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ netaddr!=0.7.16,>=0.7.13 # BSD oslo.concurrency>=3.8.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PyYAML>=3.10.0 # MIT +jsonschema>=2.0.0,<3.0.0,!=2.5.0 # MIT -- cgit 1.2.3-korg