From 519dfc0b661dfae9a381b34ef17990e16eb0e37f Mon Sep 17 00:00:00 2001 From: Aria Li Date: Thu, 25 Jul 2024 14:15:28 -0700 Subject: [PATCH] WIP --- acceptance/tests/facts/schema.rb | 94 ++++++++++++++++++++++++++++++++ lib/schema/facter.yaml | 21 +++++++ 2 files changed, 115 insertions(+) create mode 100644 acceptance/tests/facts/schema.rb diff --git a/acceptance/tests/facts/schema.rb b/acceptance/tests/facts/schema.rb new file mode 100644 index 0000000000..61c2d0b638 --- /dev/null +++ b/acceptance/tests/facts/schema.rb @@ -0,0 +1,94 @@ +test_name "Validate facter output conforms to schema" do + tag 'risk:high' + + require 'yaml' + require 'ipaddr' + # require 'psych' + # require 'json' + # require 'facter/acceptance/base_fact_utils' + + # Extra credit to make this a spec test so it runs on PRs too... + + # @param + # @return + def validate_fact(schema_fact, schema_fact_value, output_fact, output_fact_value) + fact_type = output_fact_value.class.to_s + schema_fact_type = schema_fact ? schema_fact_value["type"].capitalize : nil + if fact_type == "Hash" + fact_type = "Map" + end + if fact_type == "TrueClass" || fact_type == "FalseClass" #boolean.class returns FalseClass or TrueClass + fact_type = "Boolean" + end + if fact_type == "Float" + fact_type = "Double" + end + ipv4_regex = Regexp.new('^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$') + ipv6_regex = Regexp.new('^(([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}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$') + mac_regex = Regexp.new('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$') + if schema_fact_type == "Ip" + fact_type = ipv4_regex.match?(output_fact_value) ? "Ip" : fact_type + end + if schema_fact_type == "Ip6" + fact_type = ipv6_regex.match?(output_fact_value) ? "Ip6" : fact_type + end + if schema_fact_type == "Mac" + fact_type = mac_regex.match?(output_fact_value) ? "Mac" : fact_type + end + if fact_type == "Map" + schema_elements = schema_fact_value["elements"] + output_fact_value.each do |fact, value| + if value.nil? || !schema_elements + puts "skipped" + puts "fact: #{fact} fact value: #{value}" + puts "no schema elements: #{schema_elements} \n\n" + next + end + schema_fact, schema_fact_value = get_fact(schema_elements, fact) + validate_fact(schema_fact, schema_fact_value, fact, value) + end + end + if fact_type != schema_fact_type + puts "#{output_fact} has value: #{output_fact_value} and type: #{fact_type} does not conform to schema fact value type: #{schema_fact_type} \n\n" + end + #assert_match(fact_type, schema_fact_type, "#{output_fact} has value: #{output_fact_value} and type: #{fact_type} does not conform to schema fact value type: #{schema_fact_type}") + end + + # @param + # @return + def get_fact(fact_hash, fact_name) + fact_hash.each_key do |fact| + fact_pattern = fact_hash[fact]["pattern"] + fact_regex = fact_pattern ? Regexp.new(fact_pattern) : nil + if (fact_pattern && fact_regex.match?(fact_name)) || fact_name == fact + return fact, fact_hash[fact] + end + end + return nil + end + + step 'Validate fact collection conforms to schema' do + agents.each do |agent| + + # Load schema to compare to output_facts + PATH_TO_SCHEMA = File.join(File.dirname(__FILE__), '../../../lib/schema/facter.yaml') + schema = YAML.load_file(PATH_TO_SCHEMA) + + + on(agent, facter('--yaml')) do |facter_output| + + #get facter output for each platform + output_facts = YAML.load(facter_output.stdout) + + # validate facter output matches schema + output_facts.each do |fact, value| + schema_fact, schema_fact_value = get_fact(schema, fact) + #fact_type, schema_fact_type = validate_fact(schema_fact, schema_fact_value, fact, value) + validate_fact(schema_fact, schema_fact_value, fact, value) + # puts "fact: #{fact}" + # puts "value: #{value}\n\n" + end + end + end + end +end diff --git a/lib/schema/facter.yaml b/lib/schema/facter.yaml index 22d663928e..c47bc1b09d 100644 --- a/lib/schema/facter.yaml +++ b/lib/schema/facter.yaml @@ -325,6 +325,9 @@ dmi: uuid: type: string description: The product unique identifier of the system. + version: + type: string + description: The product model information of the system. domain: type: string @@ -1031,6 +1034,9 @@ networking: dhcp: type: ip description: The DHCP server for the network interface. + duplex: + type: string + description: The duplex settings for physical network interfaces on Linux using /sys/class/net. ip: type: ip description: The IPv4 address for the network interface. @@ -1055,9 +1061,18 @@ networking: network6: type: ip6 description: The IPv6 network for the network interface. + operational_state: + type: string + description: Linux only? TODO + physical: + type: boolean + description: Return whether network interface is a physical device on Linux based systems. scope6: type: string description: The IPv6 scope for the network interface. + speed: + type: integer + description: The speed of physical network interfaces on Linux using /sys/class/net. ip: type: ip description: The IPv4 address of the default network interface. @@ -1182,6 +1197,9 @@ os: type: map description: Represents information about the Mac OSX version. elements: + extra: + type: string + description: The ProductVersionExtra value. Only supported on macOS 13 and later. full: type: string description: The full Mac OSX version number. @@ -1366,6 +1384,9 @@ processors: count: type: integer description: The count of logical processors. + extensions: + type: array + description: The CPU extensions the processor supports. isa: type: string description: The processor instruction set architecture.