Skip to content

Commit

Permalink
Merge pull request #2746 from AriaXLi/FACT-3474/fact_schema
Browse files Browse the repository at this point in the history
(FACT-3474) Update facter schema, CONTRIBUTING.md, and add acceptance test to verify facts conform to schema
  • Loading branch information
joshcooper authored Aug 16, 2024
2 parents 3db77e0 + 6d52a38 commit 06a0618
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 1 deletion.
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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).
115 changes: 115 additions & 0 deletions acceptance/tests/facts/schema.rb
Original file line number Diff line number Diff line change
@@ -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.<devicename>, 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
28 changes: 27 additions & 1 deletion lib/schema/facter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 06a0618

Please sign in to comment.