diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..8474e1a309 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# How to contribute # + +Third-party patches are essential for keeping Puppet great. We simply can't access the huge number of platforms and myriad configurations for running Puppet. We want to keep it as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. + +## Adding New Facts ## + +When adding new facts, they need to be added to the [schema](lib/schema/facter.yaml). The fact name, description, and type must be specified in the [schema](lib/schema/facter.yaml). + +Learn more about how to contribute in our [Contribution Guidelines](https://github.com/puppetlabs/.github/blob/main/CONTRIBUTING.md). diff --git a/acceptance/tests/facts/schema.rb b/acceptance/tests/facts/schema.rb new file mode 100644 index 0000000000..5a908f5edf --- /dev/null +++ b/acceptance/tests/facts/schema.rb @@ -0,0 +1,115 @@ +test_name "Validate facter output conforms to schema" do + tag 'risk:high' + confine :except, :platform => 'windows' # See FACT-3479, once resolved this line can be removed + + require 'yaml' + require 'ipaddr' + + # Validates passed in output facts correctly conform to the facter schema, facter.yaml. + # @param schema_fact The schema fact that matches/corresponds with output_fact + # @param schema_fact_value The fact value for the schema fact + # @param output_fact The fact that is being validated + # @param output_fact The fact value of the output_fact + def validate_fact(schema_fact, schema_fact_value, output_fact, output_fact_value) + schema_fact_type = schema_fact ? schema_fact_value["type"] : nil + fail_test("Fact: #{output_fact} does not exist in schema") unless schema_fact_type + + # For each fact, it is validated by verifying that output_fact_value can + # successfully parse to fact_type and the output fact has a matching schema + # fact where both their types and name or regex match. + case output_fact_value + when Hash + fact_type = "map" + when Array + fact_type = "array" + when TrueClass, FalseClass + fact_type = "boolean" + when Float + fact_type = "double" + when Integer + fact_type = "integer" + when String + if schema_fact_type == "ip" + begin + IPAddr.new(output_fact_value).ipv4? + rescue IPAddr::Error + fail_test("Invalid ipv4 value given for #{output_fact} with value #{output_fact_value}") + else + fact_type = "ip" + end + elsif schema_fact_type == "ip6" + begin + IPAddr.new(output_fact_value).ipv6? + rescue IPAddr::Error + fail_test("Invalid ipv6 value given for #{output_fact} with value #{output_fact_value}") + else + fact_type = "ip6" + end + elsif schema_fact_type == "mac" + mac_regex = Regexp.new('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$') + fail_test("Invalid mac value given for #{output_fact} with value #{output_fact_value}") unless mac_regex.match?(output_fact_value) + fact_type = "mac" + else + fact_type = "string" + end + else + fail_test("Invalid fact type given: #{output_fact}") + end + + # Recurse over facts that have more nested facts within it + if fact_type == "map" + if output_fact_value.is_a?(Hash) + schema_elements = schema_fact_value["elements"] + output_fact_value.each do |fact, value| + if value.nil? || !schema_elements + next + # Sometimes facts with map as their type aren't nested facts, like + # hypervisors and simply just a fact with a hash value. For these + # cases, they don't need to be iterated over. + end + schema_fact, schema_fact_value = get_fact(schema_elements, fact) + validate_fact(schema_fact, schema_fact_value, fact, value) + end + end + 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 fact_hash The hash being searched for the passed in fact_name + # @param fact_name The fact that is being searched for + # @return The fact that has the same name as fact_name, if found. If not found, nil is returned. + def get_fact(fact_hash, fact_name) + fact_hash.each_key do |fact| + + # Some facts, like disks., will have different names depending + # on the machine its running on. For these facts, a pattern AKA a regex is + # provided in the facter schema. + 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 + schema_file = File.join(File.dirname(__FILE__), '../../../lib/schema/facter.yaml') + schema = YAML.load_file(schema_file) + on(agent, facter('--yaml --no-custom-facts --no-external-facts')) do |facter_output| + + #get facter output for each platform + output_facts = YAML.load(facter_output.stdout) + + # validate facter output facts match facter schema + output_facts.each do |fact, value| + schema_fact, schema_fact_value = get_fact(schema, fact) + validate_fact(schema_fact, schema_fact_value, fact, value) + end + end + end + end +end diff --git a/lib/schema/facter.yaml b/lib/schema/facter.yaml index 22d663928e..e8bbf63e93 100644 --- a/lib/schema/facter.yaml +++ b/lib/schema/facter.yaml @@ -246,6 +246,9 @@ disks: serial_number: type: string description: The serial number of the disk or block device. + serial: + type: string + description: The serial number of the disk or block device on Linux based systems. size: type: string description: The display size of the disk or block device, such as "1 GiB". @@ -258,7 +261,9 @@ disks: type: type: string description: The type of disk or block device (sshd or hdd). This fact is available only on Linux. - + wwn: + type: string + description: The identifier for the disk. dmi: type: map description: Return the system management information. @@ -325,6 +330,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 +1039,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 +1066,18 @@ networking: network6: type: ip6 description: The IPv6 network for the network interface. + operational_state: + type: string + description: The operational state for Linux based systems. + 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 +1202,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 +1389,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.