Skip to content

Commit

Permalink
(FACT-3474) Update facter schema and add test to ensure facts conform…
Browse files Browse the repository at this point in the history
… to schema

This commit:
- Updates facter.yaml to include any facts added since the schema was last
  updated
- Updates CONTRIBUTING.md so contributors know to add new facts to facter.yaml
  schema
- Adds an acceptance test that tests the facter output conforms to the schema
  on each platform
  • Loading branch information
AriaXLi committed Aug 8, 2024
1 parent 465c119 commit ca225bb
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 0 deletions.
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[facter.yaml]. The fact name, description, and type must be specified in the schema[facter.yaml].

Learn more about how to contribute in our Contribution Guidelines[https://github.com/puppetlabs/.github/blob/main/CONTRIBUTING.md].
106 changes: 106 additions & 0 deletions acceptance/tests/facts/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
test_name "Validate facter output conforms to schema" do
tag 'risk:high'

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)
fact_type = output_fact_value.class.to_s.downcase
schema_fact_type = schema_fact ? schema_fact_value["type"] : nil

# 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.
if fact_type == "hash"
Hash(output_fact_value)
fact_type = "map"
elsif fact_type == "array"
Array(output_fact_value)
elsif fact_type == "trueclass" || fact_type == "falseclass"
fact_type = "boolean"
elsif fact_type == "float"
Float(output_fact_value)
fact_type = "double"
elsif fact_type == "integer"
Integer(output_fact_value)
elsif fact_type == "string"
if schema_fact_type == "ip"
fail_test("Invalid ipv4 value given for #{output_fact} with value #{output_fact_value}") unless IPAddr.new(output_fact_value).ipv4?
fact_type = "ip"
elsif schema_fact_type == "ip6"
fail_test("Invalid ipv6 value given for #{output_fact} with value #{output_fact_value}") unless IPAddr.new(output_fact_value).ipv6?
fact_type = "ip6"
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
String(output_fact_value)
end
else
fail_test("Invalid fact type given: #{fact_type}")
end

# Recurse over facts that have more nested facts within it
if fact_type == "map"
binding.irb
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')) 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
24 changes: 24 additions & 0 deletions 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 Down Expand Up @@ -325,6 +328,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 +1037,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 +1064,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 +1200,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 +1387,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 ca225bb

Please sign in to comment.