require 'ipaddr' module Vcloud module Core ## # self::validate is entry point; this class method is called to # instantiate ConfigValidator. For example: # # Core::ConfigValidator.validate(key, data, schema) # # = Recursion in this class # # Note that this class will recursively call itself in order to validate deep # hash and array structures. # # The +data+ variable is usually either an array or hash and so will pass # through the ConfigValidator#validate_array and # ConfigValidator#validate_hash methods respectively. # # These methods then recursively instantiate this class by calling # ConfigValidator::validate again (ConfigValidator#validate_hash calls this # indirectly via the ConfigValidator#check_hash_parameter method). # class ConfigValidator attr_reader :key, :data, :schema, :type, :errors, :warnings VALID_ALPHABETICAL_VALUES_FOR_IP_RANGE = %w(Any external internal) def self.validate(key, data, schema) new(key, data, schema) end def initialize(key, data, schema) raise "Nil schema" unless schema raise "Invalid schema" unless schema.key?(:type) @type = schema[:type].to_s.downcase @errors = [] @warnings = [] @data = data @schema = schema @key = key validate end def valid? @errors.empty? end private # Call the corresponding function in this class (dependant on schema[:type]) def validate self.send("validate_#{type}".to_sym) end def validate_array unless data.is_a? Array @errors << "#{key} is not an array" return end return unless check_emptyness_ok if schema.key?(:each_element_is) element_schema = schema[:each_element_is] data.each do |element| sub_validator = ConfigValidator.validate(key, element, element_schema) @warnings = warnings + sub_validator.warnings unless sub_validator.valid? @errors = errors + sub_validator.errors end end end end def validate_hash unless data.is_a? Hash @errors << "#{key}: is not a hash" return end return unless check_emptyness_ok check_for_unknown_parameters if schema.key?(:internals) check_for_invalid_deprecations deprecations_used = get_deprecations_used warn_on_deprecations_used(deprecations_used) schema[:internals].each do |param_key,param_schema| ignore_required = ( param_schema[:deprecated_by] || deprecations_used.key?(param_key) ) check_hash_parameter(param_key, param_schema, ignore_required) end end end # Return a hash of deprecated params referenced in @data. Where the # structure is: `{ :deprecator => :deprecatee }` def get_deprecations_used used = {} schema[:internals].each do |param_key,param_schema| deprecated_by = param_schema[:deprecated_by] if deprecated_by && data[param_key] used[deprecated_by.to_sym] = param_key end end used end # Append warnings for any deprecations used. Takes the output of # `#get_deprecations_used`. def warn_on_deprecations_used(deprecations_used) deprecations_used.each do |deprecator, deprecatee| @warnings << "#{deprecatee}: is deprecated by '#{deprecator}'" end end def check_emptyness_ok unless schema.key?(:allowed_empty) && schema[:allowed_empty] if data.empty? @errors << "#{key}: cannot be empty #{type}" return false end end true end # Raise an exception if any `deprecated_by` params refer to params # that don't exist in the schema. def check_for_invalid_deprecations schema[:internals].each do |param_key,param_schema| deprecated_by = param_schema[:deprecated_by] if deprecated_by && !schema[:internals].key?(deprecated_by.to_sym) raise "#{param_key}: deprecated_by target '#{deprecated_by}' not found in schema" end end end def check_for_unknown_parameters internals = schema[:internals] # if there are no parameters specified, then assume all are ok. return true unless internals return true if schema[:permit_unknown_parameters] data.keys.each do |k| @errors << "#{key}: parameter '#{k}' is invalid" unless internals[k] end end def check_hash_parameter(sub_key, sub_schema, ignore_required=false) unless data.key?(sub_key) if sub_schema[:required] == false || ignore_required return true end @errors << "#{key}: missing '#{sub_key}' parameter" return false end sub_validator = ConfigValidator.validate( sub_key, data[sub_key], sub_schema ) @warnings = warnings + sub_validator.warnings unless sub_validator.valid? @errors = errors + sub_validator.errors end end def check_matcher_matches regex = schema[:matcher] return unless regex raise "#{key}: #{regex} is not a Regexp" unless regex.is_a? Regexp unless data =~ regex @errors << "#{key}: #{data} does not match" return false end true end def valid_alphabetical_ip_range? VALID_ALPHABETICAL_VALUES_FOR_IP_RANGE.include?(data) end def validate_boolean unless [true, false].include?(data) @errors << "#{key}: #{data} is not a valid boolean value." end end def valid_cidr_or_ip_address? begin ip = IPAddr.new(data) ip.ipv4? rescue ArgumentError false end end def validate_enum acceptable_values = schema[:acceptable_values] raise "Must set :acceptable_values for type 'enum'" unless acceptable_values.is_a?(Array) unless acceptable_values.include?(data) acceptable_values_string = acceptable_values.collect {|v| "'#{v}'" }.join(', ') @errors << "#{key}: #{@data} is not a valid value. Acceptable values are #{acceptable_values_string}." end end def validate_ip_address unless data.is_a?(String) @errors << "#{key}: #{@data} is not a valid ip_address" return end @errors << "#{key}: #{@data} is not a valid ip_address" unless valid_ip_address?(data) end def valid_ip_address? ip_address begin #valid formats recognized by IPAddr are : “address”, “address/prefixlen” and “address/mask”. # Attribute like member_ip in case of load-balancer is an "address" # and we should not accept “address/prefixlen” and “address/mask” for such fields. ip = IPAddr.new(ip_address) ip.ipv4? && !ip_address.include?('/') rescue ArgumentError false end end def validate_ip_address_range unless data.is_a?(String) @errors << "#{key}: #{@data} is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'." return end valid = valid_cidr_or_ip_address? || valid_alphabetical_ip_range? || valid_ip_range? @errors << "#{key}: #{@data} is not a valid IP address range. Valid values can be IP address, CIDR, IP range, 'Any','internal' and 'external'." unless valid end def valid_ip_range? range_parts = data.split('-') return false if range_parts.size != 2 start_address = range_parts.first end_address = range_parts.last valid_ip_address?(start_address) && valid_ip_address?(end_address) && valid_start_and_end_address_combination?(end_address, start_address) end def valid_start_and_end_address_combination?(end_address, start_address) IPAddr.new(start_address) < IPAddr.new(end_address) end def validate_string unless @data.is_a? String @errors << "#{key}: #{@data} is not a string" return end return unless check_emptyness_ok return unless check_matcher_matches end def validate_string_or_number unless data.is_a?(String) || data.is_a?(Numeric) @errors << "#{key}: #{@data} is not a string_or_number" return end end end end end