From 84b7df6b699435883430f34d25cd0cf231f13c35 Mon Sep 17 00:00:00 2001 From: Michael Hashizume Date: Thu, 5 Dec 2024 11:19:46 -0800 Subject: [PATCH 01/29] Install gems without documentation group As part of our presuite actions, we verify that we can run Facter as a standalone Ruby application. During this, we run a `bundle install`, which installs documentation dependencies. It is a known issue that the manpage generation library Facter uses, ronn, is out-of-date and errors on modern compilers. See: https://github.com/puppetlabs/facter/issues/2703 This commit updates our presuit action to omit installing documentation dependencies. This also removes the option of omitting development dependencies, as there is no such group in Facter's gemfile. --- .github/actions/presuite.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/presuite.rb b/.github/actions/presuite.rb index 189d77ccdc..f0c943146e 100644 --- a/.github/actions/presuite.rb +++ b/.github/actions/presuite.rb @@ -134,7 +134,7 @@ def run(command, dir = './', env = {}) def verify_facter_standalone_exits_0 Dir.chdir(ENV['FACTER_ROOT']) do - run('bundle install --without development') + run('bundle install --without documentation') run('bundle exec facter') end end From 6887f4a9a6d6fbf841dd3688d83dbfbe36916a8c Mon Sep 17 00:00:00 2001 From: Michael Hashizume Date: Wed, 11 Dec 2024 10:01:46 -0800 Subject: [PATCH 02/29] Use native Rake directory Rake has long automatically imported additional Rake tasks from the rakelib directory. This commit renames the tasks directory to rakelib and removes custom logic from the top-level Rakefile that imported Rake tasks from the tasks directory. --- Rakefile | 2 -- {tasks => rakelib}/check.rake | 0 {tasks => rakelib}/fact_list_generator.rake | 0 {tasks => rakelib}/manpages.rake | 0 {tasks => rakelib}/rubocop.rake | 0 {tasks => rakelib}/spec.rake | 0 6 files changed, 2 deletions(-) rename {tasks => rakelib}/check.rake (100%) rename {tasks => rakelib}/fact_list_generator.rake (100%) rename {tasks => rakelib}/manpages.rake (100%) rename {tasks => rakelib}/rubocop.rake (100%) rename {tasks => rakelib}/spec.rake (100%) diff --git a/Rakefile b/Rakefile index 055f62d1b2..8f80bc3f16 100644 --- a/Rakefile +++ b/Rakefile @@ -5,8 +5,6 @@ require 'open3' require 'rspec/core/rake_task' require 'facter/version' -Dir.glob(File.join('tasks/**/*.rake')).each { |file| load file } - task default: :spec desc 'Generate changelog' diff --git a/tasks/check.rake b/rakelib/check.rake similarity index 100% rename from tasks/check.rake rename to rakelib/check.rake diff --git a/tasks/fact_list_generator.rake b/rakelib/fact_list_generator.rake similarity index 100% rename from tasks/fact_list_generator.rake rename to rakelib/fact_list_generator.rake diff --git a/tasks/manpages.rake b/rakelib/manpages.rake similarity index 100% rename from tasks/manpages.rake rename to rakelib/manpages.rake diff --git a/tasks/rubocop.rake b/rakelib/rubocop.rake similarity index 100% rename from tasks/rubocop.rake rename to rakelib/rubocop.rake diff --git a/tasks/spec.rake b/rakelib/spec.rake similarity index 100% rename from tasks/spec.rake rename to rakelib/spec.rake From e183a51a4388e692b843eeef3f8c4036fb538ca7 Mon Sep 17 00:00:00 2001 From: Aria Li Date: Mon, 27 Jan 2025 16:26:38 -0800 Subject: [PATCH 03/29] (PE-40092) Use virt-what to populate cloud.provider fact on kvm, xen, xenhvm, xenu hypervisors Previously, Facter incorrectly assumes AWS when kvm, xen, and xenhvm hypervisors are used and there is ec2 metadata. For example, Facter incorrectly assumes AWS when something is running on OpenStack and is using kvm/xen as their hypervisor. This commit updates lib/facter/facts/linux/cloud/provider.rb to try to use virt-what to determine the cloud.provider fact when on kvm, xen, xenhvm, and xenu hypervisors. If unable to use virt-what and there ec2 metadata, provider.rb will fallback on the previous behavior and return AWS. virt-what needs to be run as root due to using dmidecode. This commit also updates lib/facter/resolvers/processors.rb to resolve rubocop violations. --- lib/facter/facts/linux/cloud/provider.rb | 13 +- lib/facter/resolvers/processors.rb | 2 +- .../facter/facts/linux/cloud/provider_spec.rb | 193 +++++++++++++++++- 3 files changed, 199 insertions(+), 9 deletions(-) diff --git a/lib/facter/facts/linux/cloud/provider.rb b/lib/facter/facts/linux/cloud/provider.rb index 1e16a19aef..3b5395cb42 100644 --- a/lib/facter/facts/linux/cloud/provider.rb +++ b/lib/facter/facts/linux/cloud/provider.rb @@ -11,9 +11,18 @@ def call_the_resolver when 'hyperv' metadata = Facter::Resolvers::Az.resolve(:metadata) 'azure' unless metadata.nil? || metadata.empty? - when 'kvm', 'xen', 'xenhvm' + when 'kvm', 'xen', 'xenhvm', 'xenu' metadata = Facter::Resolvers::Ec2.resolve(:metadata) - 'aws' unless metadata.nil? || metadata.empty? + if metadata && !metadata.empty? + if Process.uid.zero? && File.executable?('/opt/puppetlabs/puppet/bin/virt-what') # virt-what needs to be run as root + output = Facter::Core::Execution.execute('/opt/puppetlabs/puppet/bin/virt-what') + # rubocop:disable Metrics/BlockNesting + output.lines(chomp: true).any?('aws') ? 'aws' : nil + # rubocop:enable Metrics/BlockNesting + else + 'aws' + end + end when 'gce' metadata = Facter::Resolvers::Gce.resolve(:metadata) 'gce' unless metadata.nil? || metadata.empty? diff --git a/lib/facter/resolvers/processors.rb b/lib/facter/resolvers/processors.rb index 601499cedb..d54bbb7b75 100644 --- a/lib/facter/resolvers/processors.rb +++ b/lib/facter/resolvers/processors.rb @@ -56,7 +56,7 @@ def count_processors(tokens) end def construct_models_list(tokens) - return unless tokens.first.strip == 'model name' || tokens.first.strip == 'cpu' + return unless ['model name', 'cpu'].include?(tokens.first.strip) @fact_list[:models] << tokens.last.strip end diff --git a/spec/facter/facts/linux/cloud/provider_spec.rb b/spec/facter/facts/linux/cloud/provider_spec.rb index 0e32fbb757..7bdfc7e06b 100644 --- a/spec/facter/facts/linux/cloud/provider_spec.rb +++ b/spec/facter/facts/linux/cloud/provider_spec.rb @@ -46,15 +46,54 @@ allow(Facter::Util::Facts::Posix::VirtualDetector).to receive(:platform).and_return('kvm') end - describe 'Ec2 data exists and aws fact is set' do + describe 'Ec2 data exists and not running as root' do let(:value) { { 'some' => 'fact' } } - it 'Testing things' do + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(512) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ have_attributes(name: 'cloud.provider', value: 'aws') end end + describe 'Ec2 data exists and virt-what is not executable' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(false) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('aws') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns not aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns nil' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: nil) + end + end + context 'when Ec2 data does not exist nil is returned' do let(:value) { {} } @@ -71,15 +110,54 @@ allow(Facter::Util::Facts::Posix::VirtualDetector).to receive(:platform).and_return('xen') end - describe 'Ec2 data exists and aws fact is set' do + describe 'Ec2 data exists and not running as root' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(512) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what is not executable' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(false) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns aws' do let(:value) { { 'some' => 'fact' } } - it 'Testing things' do + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('aws') expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ have_attributes(name: 'cloud.provider', value: 'aws') end end + describe 'Ec2 data exists and virt-what returns not aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns nil' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: nil) + end + end + context 'when Ec2 data does not exist nil is returned' do let(:value) { {} } @@ -96,15 +174,54 @@ allow(Facter::Util::Facts::Posix::VirtualDetector).to receive(:platform).and_return('xenhvm') end - describe 'Ec2 data exists and aws fact is set' do + describe 'Ec2 data exists and not running as root' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(512) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what is not executable' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(false) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns aws' do let(:value) { { 'some' => 'fact' } } - it 'Testing things' do + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('aws') expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ have_attributes(name: 'cloud.provider', value: 'aws') end end + describe 'Ec2 data exists and virt-what returns not aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns nil' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: nil) + end + end + context 'when Ec2 data does not exist nil is returned' do let(:value) { {} } @@ -115,6 +232,70 @@ end end + describe 'when on xenu' do + before do + allow(Facter::Resolvers::Ec2).to receive(:resolve).with(:metadata).and_return(value) + allow(Facter::Util::Facts::Posix::VirtualDetector).to receive(:platform).and_return('xenu') + end + + describe 'Ec2 data exists and not running as root' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(512) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what is not executable' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(false) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns aws' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('aws') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: 'aws') + end + end + + describe 'Ec2 data exists and virt-what returns not aws' do + let(:value) { { 'some' => 'fact' } } + + it 'returns nil' do + allow(File).to receive(:executable?).with('/opt/puppetlabs/puppet/bin/virt-what').and_return(true) + allow(Process).to receive(:uid).and_return(0) + allow(Facter::Core::Execution).to receive(:execute).with('/opt/puppetlabs/puppet/bin/virt-what').and_return('kvm') + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: nil) + end + end + + context 'when Ec2 data does not exist, nil is returned' do + let(:value) { {} } + + it 'returns nil' do + expect(fact.call_the_resolver).to be_an_instance_of(Facter::ResolvedFact).and \ + have_attributes(name: 'cloud.provider', value: nil) + end + end + end + describe 'when on gce' do before do allow(Facter::Resolvers::Gce).to receive(:resolve).with(:metadata).and_return(value) From 335c40799b92c817ea36c256782f62e272d1ebda Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 27 Jan 2025 15:40:01 -0800 Subject: [PATCH 04/29] (FACT-3488) Fix potential NoMethodError Commit 7bc38ccb58e6abf66d6141a3b2e9f60eb0077d71 combined the call to `FileHelper.safe_read(`/etc/machine-id`, nil)` and `strip`. However, if we ever fail to read `/etc/machine-id`, then we would attempt to call `strip` on nil. We could use safe-nav operator, however, that would result in the hypervisor containing: { systemd_nspawn: { 'id' => nil } } This commit restores the original behavior of only including hypervisor information for `systemd-nspawn` if the machine_id is not nil and protects against the `nil` case. --- lib/facter/resolvers/containers.rb | 7 ++++++- spec/facter/resolvers/containers_spec.rb | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/facter/resolvers/containers.rb b/lib/facter/resolvers/containers.rb index 030d7267a4..5bcd02092d 100644 --- a/lib/facter/resolvers/containers.rb +++ b/lib/facter/resolvers/containers.rb @@ -55,7 +55,12 @@ def read_environ(fact_name) return nil when 'systemd-nspawn' vm = 'systemd_nspawn' - info = { 'id' => Facter::Util::FileHelper.safe_read('/etc/machine-id', nil).strip } + machine_id = Facter::Util::FileHelper.safe_read('/etc/machine-id', nil) + info = if machine_id + { 'id' => machine_id.strip } + else + {} + end else vm = 'container_other' log.warn("Container runtime, '#{container}', is unsupported, setting to '#{vm}'") diff --git a/spec/facter/resolvers/containers_spec.rb b/spec/facter/resolvers/containers_spec.rb index 64348c3794..78730efe24 100644 --- a/spec/facter/resolvers/containers_spec.rb +++ b/spec/facter/resolvers/containers_spec.rb @@ -48,6 +48,14 @@ it 'return nspawn info for hypervisor' do expect(containers_resolver.resolve(:hypervisor)).to eq(result) end + + it 'omits hypervisor info if it fails to read /etc/machine-id' do + allow(Facter::Util::FileHelper).to receive(:safe_read) + .with('/etc/machine-id', nil) + .and_return(nil) + + expect(containers_resolver.resolve(:hypervisor)).to eq(systemd_nspawn: {}) + end end context 'when hypervisor is lxc and it is discovered by cgroup' do From 94c3eb2e0d73b0cdff445c4167d52626ee3eef1a Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 29 Jan 2025 16:01:19 -0800 Subject: [PATCH 05/29] (FACT-3488) Add more debugging to VirtualDetector --- .../util/facts/posix/virtual_detector.rb | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/facter/util/facts/posix/virtual_detector.rb b/lib/facter/util/facts/posix/virtual_detector.rb index cc4ba11b70..b5eadfab7b 100644 --- a/lib/facter/util/facts/posix/virtual_detector.rb +++ b/lib/facter/util/facts/posix/virtual_detector.rb @@ -16,7 +16,7 @@ def platform private def check_docker_lxc - Facter::Resolvers::Containers.resolve(:vm) + resolve(Facter::Resolvers::Containers, :vm) end def check_gce @@ -29,35 +29,35 @@ def check_illumos_lx end def check_vmware - Facter::Resolvers::Vmware.resolve(:vm) + resolve(Facter::Resolvers::Vmware, :vm) end def retrieve_from_virt_what - Facter::Resolvers::VirtWhat.resolve(:vm) + resolve(Facter::Resolvers::VirtWhat, :vm) end def check_open_vz - Facter::Resolvers::OpenVz.resolve(:vm) + resolve(Facter::Resolvers::OpenVz, :vm) end def check_vserver - Facter::Resolvers::VirtWhat.resolve(:vserver) + resolve(Facter::Resolvers::VirtWhat, :vserver) end def check_xen - Facter::Resolvers::Xen.resolve(:vm) + resolve(Facter::Resolvers::Xen, :vm) end def check_freebsd return unless Object.const_defined?('Facter::Resolvers::Freebsd::Virtual') - Facter::Resolvers::Freebsd::Virtual.resolve(:vm) + resolve(Facter::Resolvers::Freebsd::Virtual, :vm) end def check_openbsd return unless Object.const_defined?('Facter::Resolvers::Openbsd::Virtual') - Facter::Resolvers::Openbsd::Virtual.resolve(:vm) + resolve(Facter::Resolvers::Openbsd::Virtual, :vm) end def check_other_facts @@ -73,7 +73,13 @@ def check_other_facts end def check_lspci - Facter::Resolvers::Lspci.resolve(:vm) + resolve(Facter::Resolvers::Lspci, :vm) + end + + def resolve(resolver, fact_name) + value = resolver.resolve(fact_name) + resolver.log.debug("Resolved '#{fact_name}' to '#{value}'") + value end end end From f8d93f236f347eaba757d8572d86e6b21d6166b2 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 29 Jan 2025 16:32:56 -0800 Subject: [PATCH 06/29] (FACT-3488) Check for /.dockerenv If /.dockerenv exists, assume we're in docker and include cgroup info if present. Some people recommend checking for /.dockerinit, but that was dropped awhile ago, so don't start depending on it now. --- lib/facter/resolvers/containers.rb | 20 +++++++++++++++++--- spec/facter/resolvers/containers_spec.rb | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/facter/resolvers/containers.rb b/lib/facter/resolvers/containers.rb index 5bcd02092d..3726509e6f 100644 --- a/lib/facter/resolvers/containers.rb +++ b/lib/facter/resolvers/containers.rb @@ -23,10 +23,24 @@ def read_cgroup(fact_name) output_cgroup = Facter::Util::FileHelper.safe_read('/proc/1/cgroup', nil) return unless output_cgroup - output_docker = %r{docker/(.+)}.match(output_cgroup) - output_lxc = %r{^/lxc/([^/]+)}.match(output_cgroup) + # '+' matches one or more characters, so if there's a match, the + # capture group must be non-empty + output_docker = %r{docker/(?.+)}.match(output_cgroup) + output_lxc = %r{^/lxc/(?[^/]+)}.match(output_cgroup) - info, vm = extract_vm_and_info(output_docker, output_lxc) + if File.exist?('/.dockerenv') + vm = 'docker' + info = output_docker && output_docker[:id] ? { 'id' => output_docker[:id] } : {} + elsif output_docker + vm = 'docker' + info = { 'id' => output_docker[:id] } + elsif output_lxc + vm = 'lxc' + info = { 'name' => output_lxc[:name] } + else + # fallback to read_environ + return nil + end @fact_list[:vm] = vm @fact_list[:hypervisor] = { vm.to_sym => info } if vm @fact_list[fact_name] diff --git a/spec/facter/resolvers/containers_spec.rb b/spec/facter/resolvers/containers_spec.rb index 78730efe24..34259b1c20 100644 --- a/spec/facter/resolvers/containers_spec.rb +++ b/spec/facter/resolvers/containers_spec.rb @@ -10,12 +10,31 @@ allow(Facter::Util::FileHelper).to receive(:safe_readlines) .with('/proc/1/environ', [], "\0", chomp: true) .and_return(environ_output) + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with('/.dockerenv').and_return(false) end after do containers_resolver.invalidate_cache end + context 'when hypervisor is docker desktop' do + let(:cgroup_output) { '0::/' } + let(:environ_output) { ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] } + + before do + allow(File).to receive(:exist?).with('/.dockerenv').and_return(true) + end + + it 'returns docker for vm' do + expect(containers_resolver.resolve(:vm)).to eq('docker') + end + + it 'returns empty hypervisor info' do + expect(containers_resolver.resolve(:hypervisor)).to eq(docker: {}) + end + end + context 'when hypervisor is docker' do let(:cgroup_output) { load_fixture('docker_cgroup').read } let(:environ_output) { ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'] } From a0066325e899e862db18081f62c991cc1aaa0bbd Mon Sep 17 00:00:00 2001 From: Shubham Shinde Date: Tue, 4 Mar 2025 13:50:13 +0530 Subject: [PATCH 07/29] Fix rubocop 'Style/RedundantCondition' offenses --- lib/facter/custom_facts/core/execution/base.rb | 2 +- lib/facter/resolvers/linux/networking.rb | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/facter/custom_facts/core/execution/base.rb b/lib/facter/custom_facts/core/execution/base.rb index a498208c68..7707743823 100644 --- a/lib/facter/custom_facts/core/execution/base.rb +++ b/lib/facter/custom_facts/core/execution/base.rb @@ -139,7 +139,7 @@ def log_stderr(msg, command, logger) def builtin_command?(command) output, _status = Open3.capture2("type #{command}") - /builtin/.match?(output.chomp) ? true : false + /builtin/.match?(output.chomp) end end end diff --git a/lib/facter/resolvers/linux/networking.rb b/lib/facter/resolvers/linux/networking.rb index 1a900f98e5..5af33d3f81 100644 --- a/lib/facter/resolvers/linux/networking.rb +++ b/lib/facter/resolvers/linux/networking.rb @@ -57,11 +57,7 @@ def operstate(interface_name, iface) end def physical(ifname, iface) - iface[:physical] = if File.exist?("/sys/class/net/#{ifname}/device") - true - else - false - end + iface[:physical] = File.exist?("/sys/class/net/#{ifname}/device") end def duplex(interface_name, iface) From cd6e1934cff7adf6283b60efa9a7a837aeae40a3 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 21 Mar 2025 12:11:34 -0700 Subject: [PATCH 08/29] Add rubocop-{factory_bot,capybara} as plugins See puppet-private#92d2b0311ed4357d6315a2db6f2c28eee6e50249 and 4755ca430b952345669569ffe8f23103aafb3033 --- .rubocop.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index edf01dc4b1..4b924cd515 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,10 @@ require: - rubocop-performance - rubocop-rspec +plugins: + - rubocop-factory_bot + - rubocop-capybara + Layout/LineLength: Enabled: false From 977899203c818dd45dd8289778cdfc9d0a4b24d4 Mon Sep 17 00:00:00 2001 From: skyamgarp <130442619+skyamgarp@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:19:58 +0530 Subject: [PATCH 09/29] (PA-7238) Fix os.release fact spec for windows 2025 Co-authored-by: brajjan Windows build number we are referring from: https://learn.microsoft.com/en-us/windows-server/get-started/windows-server-release-info --- acceptance/lib/facter/acceptance/base_fact_utils.rb | 2 ++ lib/facter/util/facts/windows_release_finder.rb | 6 ++++-- spec/facter/util/facts/windows_release_finder_spec.rb | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/acceptance/lib/facter/acceptance/base_fact_utils.rb b/acceptance/lib/facter/acceptance/base_fact_utils.rb index c789830923..9542fe1871 100644 --- a/acceptance/lib/facter/acceptance/base_fact_utils.rb +++ b/acceptance/lib/facter/acceptance/base_fact_utils.rb @@ -437,6 +437,8 @@ def windows_expected_facts(agent) os_version = '2019' elsif agent['platform'] =~ /2022/ os_version = '2022' + elsif agent['platform'] =~ /2025/ + os_version = '2025' else fail_test "Unknown Windows version #{agent['platform']}" end diff --git a/lib/facter/util/facts/windows_release_finder.rb b/lib/facter/util/facts/windows_release_finder.rb index de285fde6b..ac721a76fe 100644 --- a/lib/facter/util/facts/windows_release_finder.rb +++ b/lib/facter/util/facts/windows_release_finder.rb @@ -25,10 +25,12 @@ def find_release(input) def check_version_10_11(consumerrel, kernel_version) build_number = kernel_version[/([^.]*)$/].to_i - return '11' if build_number >= 22_000 + return '11' if consumerrel && build_number >= 22_000 return '10' if consumerrel - if build_number >= 20_348 + if build_number >= 26_100 + '2025' + elsif build_number >= 20_348 '2022' elsif build_number >= 17_623 '2019' diff --git a/spec/facter/util/facts/windows_release_finder_spec.rb b/spec/facter/util/facts/windows_release_finder_spec.rb index a6a81d4d22..4740bcde08 100644 --- a/spec/facter/util/facts/windows_release_finder_spec.rb +++ b/spec/facter/util/facts/windows_release_finder_spec.rb @@ -36,6 +36,17 @@ end end + describe '#find windows release when version is 2025' do + let(:cons) { false } + let(:desc) {} + let(:k_version) { '10.0.26100' } + let(:version) { '10.0' } + + it 'returns 2025' do + expect(Facter::Util::Facts::WindowsReleaseFinder.find_release(input)).to eql('2025') + end + end + describe '#find windows release when version is 2022' do let(:cons) { false } let(:desc) {} From 25a4ba8d31b0f9ed00b34152b3769ac1944c85ca Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 24 Apr 2025 15:17:02 -0700 Subject: [PATCH 10/29] (maint) resolve rubocop failures Rubocop started failing due to ``` lib/facter/facts/windows/os/release.rb:20:54: C: [Correctable] Style/RedundantParentheses: Don't use parentheses around a method argument. arr << Facter::ResolvedFact.new(FACT_NAME, ({ full: fact_value, major: fact_value } if fact_value)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lib/facter/facts/windows/os/release.rb:20:94: F: Lint/Syntax: unexpected token kIF_MOD (Using Ruby 2.5 parser; configure using TargetRubyVersion parameter, under AllCops) arr << Facter::ResolvedFact.new(FACT_NAME, ({ full: fact_value, major: fact_value } if fact_value)) ... ``` --- lib/facter/facts/windows/os/release.rb | 3 ++- lib/facter/resolvers/windows/networking.rb | 4 ++-- spec/custom_facts/util/confine_spec.rb | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/facter/facts/windows/os/release.rb b/lib/facter/facts/windows/os/release.rb index de59acebe5..3abf4b646f 100644 --- a/lib/facter/facts/windows/os/release.rb +++ b/lib/facter/facts/windows/os/release.rb @@ -17,7 +17,8 @@ def call_the_resolver } fact_value = Facter::Util::Facts::WindowsReleaseFinder.find_release(input) - arr << Facter::ResolvedFact.new(FACT_NAME, ({ full: fact_value, major: fact_value } if fact_value)) + resolved_value = { full: fact_value, major: fact_value } if fact_value + arr << Facter::ResolvedFact.new(FACT_NAME, resolved_value) ALIASES.each { |aliass| arr << Facter::ResolvedFact.new(aliass, fact_value, :legacy) } arr end diff --git a/lib/facter/resolvers/windows/networking.rb b/lib/facter/resolvers/windows/networking.rb index 6c2c5bfe65..7b0cb7df97 100644 --- a/lib/facter/resolvers/windows/networking.rb +++ b/lib/facter/resolvers/windows/networking.rb @@ -131,8 +131,8 @@ def find_bindings(sock_addr, unicast, addr) def find_primary_interface(sock_addr, name, addr) if !@fact_list[:primary_interface] && - ([NetworkingFFI::AF_INET, NetworkingFFI::AF_INET6].include?(sock_addr[:sa_family]) && - !::Facter::Util::Resolvers::Networking.ignored_ip_address(addr)) + [NetworkingFFI::AF_INET, NetworkingFFI::AF_INET6].include?(sock_addr[:sa_family]) && + !::Facter::Util::Resolvers::Networking.ignored_ip_address(addr) @fact_list[:primary_interface] = name.to_s end end diff --git a/spec/custom_facts/util/confine_spec.rb b/spec/custom_facts/util/confine_spec.rb index 24021eab88..1a903ff6d3 100755 --- a/spec/custom_facts/util/confine_spec.rb +++ b/spec/custom_facts/util/confine_spec.rb @@ -97,7 +97,7 @@ def confined(fact_value, *confines) end it "returns true if any of the provided ranges matches the fact's value" do - expect(confined(6, (5..7))).to be true + expect(confined(6, 5..7)).to be true end it "returns false if none of the provided values matches the fact's value" do @@ -105,7 +105,7 @@ def confined(fact_value, *confines) end it "returns false if none of the provided integer values matches the fact's value" do - expect(confined(2, 1, [3, 4], (5..7))).to be false + expect(confined(2, 1, [3, 4], 5..7)).to be false end it "returns false if none of the provided boolan values matches the fact's value" do @@ -117,7 +117,7 @@ def confined(fact_value, *confines) end it "returns false if none of the provided ranges matches the fact's value" do - expect(confined(8, (5..7))).to be false + expect(confined(8, 5..7)).to be false end it 'accepts and evaluate a block argument against the fact' do From 106ebfa709e3bfb6d85bdef2e3ab3d0887f56ed8 Mon Sep 17 00:00:00 2001 From: Aria Li Date: Tue, 29 Apr 2025 15:30:43 -0700 Subject: [PATCH 11/29] (FACT-3495) Add missing chassis types Currently, chassis types exist in a hash called types in the function chassis_to_name in dmi.rb. Each type's position and ordering in the hash corresponds with its corresponding number/hex. This commit adds the following missing chassis types to the hash: - IoT Gateway (byte value: 21h) - Embedded PC (byte value: 22h) - Mini PC (byte value: 23h) - Stick PC (byte value: 24h) For more information on chassis types, see: https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.8.0.pdf --- lib/facter/resolvers/dmi.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/facter/resolvers/dmi.rb b/lib/facter/resolvers/dmi.rb index d27e40111f..4b36e38d3c 100644 --- a/lib/facter/resolvers/dmi.rb +++ b/lib/facter/resolvers/dmi.rb @@ -50,7 +50,7 @@ def chassis_to_name(chassis_type) 'Space-Saving', 'Lunch Box', 'Main System Chassis', 'Expansion Chassis', 'SubChassis', 'Bus Expansion Chassis', 'Peripheral Chassis', 'Storage Chassis', 'Rack Mount Chassis', 'Sealed-Case PC', 'Multi-system', 'CompactPCI', 'AdvancedTCA', 'Blade', 'Blade Enclosure', - 'Tablet', 'Convertible', 'Detachable'] + 'Tablet', 'Convertible', 'Detachable', 'IoT Gateway', 'Embedded PC', 'Mini PC', 'Stick PC'] @fact_list[:chassis_type] = types[chassis_type.to_i - 1] end end From 27d12ad840fc5dbc4a18dd64438d8083c8129620 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 1 May 2025 15:10:13 -0700 Subject: [PATCH 12/29] Run integration tests consistently The `rake spec_integration` and `rspec spec_integration` commands loaded spec_helpers differently. The latter caused unit test stubs to incorrectly be loaded when running integration tests. Specifically, the first command (rake) loaded: spec_integration/spec_helper.rb spec_integration/**/*_spec.rb But the second command (rspec) loaded: spec/spec_helper.rb spec_integration/**/*_spec.rb spec_integration/spec_helper.rb Rake didn't have the problem because it specified the `default-path` as `spec_integration`. The `--require spec_helper` option in `.rspec`, then loaded spec_integration/spec_helper as expected. When running `rspec` directory, the `default-path` wasn't modified, so it unexpectedly loaded `spec/spec_helper.rb`. I noticed this behavior earlier in 72241e6294c33aa34910d5389d76532667ad6e5c, but didn't realize the extent of the problem. For example, the "unit_test" guard shouild have prevented the `colorize` method from being defined, registering webmock, etc. This commit changes the behavior: * rake and rspec behave the same * unit and integration tests uses different rspec status files, so --only-failures works as expected * webmock is only registered for unit tests * The `skip_outside_ci` tag only filters integration tests * The unit and integration spec_helper behaviors are clearly separated. * It's not necessary to explicitly `require 'spec_helper'` in integration tests. --- .gitignore | 1 + rakelib/spec.rake | 2 +- spec/spec_helper.rb | 135 +++++++++++-------- spec_integration/facter_block_legacy_spec.rb | 1 - spec_integration/facter_resolve_spec.rb | 2 +- spec_integration/facter_spec.rb | 2 - spec_integration/facter_to_hash_spec.rb | 2 - spec_integration/spec_helper.rb | 53 -------- 8 files changed, 80 insertions(+), 118 deletions(-) delete mode 100644 spec_integration/spec_helper.rb diff --git a/.gitignore b/.gitignore index bb0cf40eef..76aa0d30f6 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ build-iPhoneSimulator/ .rvmrc .rspec_status +.rspec_integration_status Gemfile.lock Gemfile.local acceptance/vendor diff --git a/rakelib/spec.rake b/rakelib/spec.rake index 3220e1aa17..278f08a78d 100644 --- a/rakelib/spec.rake +++ b/rakelib/spec.rake @@ -14,7 +14,7 @@ begin desc 'Run rspec integration test in random order' RSpec::Core::RakeTask.new(:spec_integration) do |t| t.rspec_opts = '--pattern spec_integration/**/*_spec.rb '\ - '--default-path spec_integration --order random' + '--order random' end rescue LoadError puts 'Could not load rspec' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9ba3469616..435560d2f7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,62 +1,15 @@ # frozen_string_literal: true require 'pathname' - -ROOT_DIR = Pathname.new(File.expand_path('..', __dir__)) unless defined?(ROOT_DIR) - -ENV['RACK_ENV'] = 'test' - -require 'bundler/setup' - -require 'simplecov' - -# Configure SimpleCov -SimpleCov.start do - track_files 'lib/**/*.rb' - add_filter 'spec' -end - -require 'open3' -require 'thor' require 'fileutils' - -require_relative '../lib/facter/resolvers/base_resolver' - -unit_tests = ARGV.grep(/spec_integration/).empty? -Dir[ROOT_DIR.join('spec/mocks/*.rb')].sort.each { |file| require file } if unit_tests - -require_relative 'custom_facts/puppetlabs_spec/files' - -require 'facter' -require 'facter/framework/cli/cli' -require 'facter/framework/cli/cli_launcher' - -if unit_tests - Dir.glob(File.join('./lib/facter/util', '/**/*/', '*.rb')).sort.each(&method(:require)) - Dir.glob(File.join('./lib/facter/facts', '/**/*/', '*.rb')).sort.each(&method(:require)) - Dir.glob(File.join('./lib/facter/resolvers', '/**/*/', '*.rb')).sort.each(&method(:require)) -end - -default_coverage = 90 -SimpleCov.minimum_coverage ENV['COVERAGE'] || default_coverage - -def colorize(str, color) - "#{color}#{str}#{Facter::RESET}" -end - -# Configure webmock -require 'webmock/rspec' -WebMock.disable_net_connect! - # Configure test logger +require 'logger' +require 'stringio' logdest = ENV['FACTER_TEST_LOG'] ? File.new(ENV['FACTER_TEST_LOG'], 'w') : StringIO.new logger = Logger.new(logdest) # Configure RSpec RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' - # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! config.expose_dsl_globally = true @@ -87,20 +40,86 @@ def colorize(str, color) config.before do |test| m = test.metadata logger.info("*** BEGIN TEST #{m[:file_path]}:#{m[:line_number]}") - - LegacyFacter.clear - Facter.clear_messages - end - - config.after do - Facter::OptionStore.reset - Facter::ConfigReader.clear - Facter::ConfigFileOptions.clear end # This will cleanup any files that were created with tmpdir or tmpfile + require_relative 'custom_facts/puppetlabs_spec/files' config.extend PuppetlabsSpec::Files config.after do PuppetlabsSpec::Files.cleanup end end + +# Configure unit vs integration +ROOT_DIR = Pathname.new(File.expand_path('..', __dir__)) unless defined?(ROOT_DIR) +SPEC_DIR = File.join(ROOT_DIR, 'spec') +INTEGRATION_DIR = File.join(ROOT_DIR, 'spec_integration') + +unit_tests = ARGV.grep(/spec_integration/).empty? +if unit_tests + # Normally facter lazily loads facts and resolvers for the specific platform + # it's running on, because some classes depend on libraries that can't be + # required on the host running test, like WIN32OLE. So first, mock out + # platform-specific classes + Dir[ROOT_DIR.join('spec/mocks/*.rb')].sort.each { |file| require file } + + # Second, require facter + require 'facter' + + # Third, eagerly load all facts and resolvers + Dir.glob(File.join('./lib/facter/util', '/**/*/', '*.rb')).sort.each(&method(:require)) + Dir.glob(File.join('./lib/facter/facts', '/**/*/', '*.rb')).sort.each(&method(:require)) + Dir.glob(File.join('./lib/facter/resolvers', '/**/*/', '*.rb')).sort.each(&method(:require)) + + # Configure webmock + require 'webmock/rspec' + WebMock.disable_net_connect! + + # Unit specific config + RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + + config.before do + LegacyFacter.clear + Facter.clear_messages + end + + config.after do + Facter::OptionStore.reset + Facter::ConfigReader.clear + Facter::ConfigFileOptions.clear + end + end + + def colorize(str, color) + "#{color}#{str}#{Facter::RESET}" + end +else + require 'facter' + + $LOAD_PATH << INTEGRATION_DIR + require 'integration_helper' + + # prevent facter from loading its spec files as facts + $LOAD_PATH.delete_if { |entry| entry =~ %r{facter/spec} } + + # Integration specific config + RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_integration_status' + + # exclude `skip_outside_ci` tests if not running on CI + config.filter_run_excluding :skip_outside_ci unless ENV['CI'] + + config.after do + Facter::OptionStore.reset + Facter::ConfigReader.clear + Facter::ConfigFileOptions.clear + + Facter.reset + Facter.clear + LegacyFacter.clear + end + end +end diff --git a/spec_integration/facter_block_legacy_spec.rb b/spec_integration/facter_block_legacy_spec.rb index 274693edbd..2fa14cfe50 100644 --- a/spec_integration/facter_block_legacy_spec.rb +++ b/spec_integration/facter_block_legacy_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'json' -require_relative 'spec_helper' describe 'Facter' do context 'when legacy facts is blocked' do diff --git a/spec_integration/facter_resolve_spec.rb b/spec_integration/facter_resolve_spec.rb index 195ed69b8e..8ec8e42ff2 100644 --- a/spec_integration/facter_resolve_spec.rb +++ b/spec_integration/facter_resolve_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'spec_helper' +require 'tmpdir' describe Facter do context 'when recursively calling facter in an external/custom fact' do diff --git a/spec_integration/facter_spec.rb b/spec_integration/facter_spec.rb index e6cf8b1b0f..d8af9fc403 100644 --- a/spec_integration/facter_spec.rb +++ b/spec_integration/facter_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'spec_helper' - describe 'Facter' do include PuppetlabsSpec::Files diff --git a/spec_integration/facter_to_hash_spec.rb b/spec_integration/facter_to_hash_spec.rb index 261f932196..8aa363d1c9 100644 --- a/spec_integration/facter_to_hash_spec.rb +++ b/spec_integration/facter_to_hash_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'spec_helper' - describe Facter do context 'when calling facter cli' do context 'with no user query' do diff --git a/spec_integration/spec_helper.rb b/spec_integration/spec_helper.rb deleted file mode 100644 index 516663da39..0000000000 --- a/spec_integration/spec_helper.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'facter' -require_relative 'integration_helper' -require_relative '../spec/custom_facts/puppetlabs_spec/files' - -# prevent facter from loading its spec files as facts -$LOAD_PATH.delete_if { |entry| entry =~ %r{facter/spec} } - -# Configure RSpec -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' - - # exclude `skip_outside_ci` tests if not running on CI - config.filter_run_excluding :skip_outside_ci unless ENV['CI'] - - # Disable RSpec exposing methods globally on `Module` and `main` - config.disable_monkey_patching! - config.expose_dsl_globally = true - - config.expect_with :rspec do |c| - c.syntax = :expect - end - - config.mock_with :rspec do |mocks| - # This option should be set when all dependencies are being loaded - # before a spec run, as is the case in a typical spec helper. It will - # cause any verifying double instantiation for a class that does not - # exist to raise, protecting against incorrectly spelt names. - mocks.verify_doubled_constant_names = true - - # This option forces the same argument and method existence checks that are - # performed for object_double are also performed on partial doubles. - # You should set this unless you have a good reason not to. - # It defaults to off only for backwards compatibility. - - mocks.verify_partial_doubles = true - end - - config.after do - Facter.reset - Facter.clear - Facter::OptionStore.reset - LegacyFacter.clear - end - - # This will cleanup any files that were created with tmpdir or tmpfile - config.extend PuppetlabsSpec::Files - config.after(:all) do - PuppetlabsSpec::Files.cleanup - end -end From 4dc6fb891d040992cc9120695fc65ea745e43311 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 1 May 2025 15:30:57 -0700 Subject: [PATCH 13/29] Don't load spec/**/*.rb as custom facts (again) Commit c3350f82f8bb6b74fb6f18bfbfa147bed759b32b removed the `facter/spec` directory from the load path so that Facter wouldn't attempt to load custom facts in "spec/**/*.rb". However, when we forked the repo, the name changed to `facter-private`, reintroducing the problem. This commit resolves the absolute path to the spec directory, so we're not sensitive to the name of the git checkout directory. --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 435560d2f7..1b68195b42 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -102,7 +102,7 @@ def colorize(str, color) require 'integration_helper' # prevent facter from loading its spec files as facts - $LOAD_PATH.delete_if { |entry| entry =~ %r{facter/spec} } + $LOAD_PATH.delete_if { |entry| entry == SPEC_DIR } # Integration specific config RSpec.configure do |config| From 7a22f1bbfbbdae486e4b4af3278bf3681f14ed96 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 3 Apr 2025 22:38:04 -0700 Subject: [PATCH 14/29] Force networking fact values to UTF-8 The socket APIs return a frozen string, so we have to dup before forcing the encoding. However, in some cases, we make a copy, due to regex matching or string splitting, so dup is not needed. --- lib/facter/resolvers/hostname.rb | 4 ++-- lib/facter/resolvers/linux/hostname.rb | 4 ++-- lib/facter/util/linux/socket_parser.rb | 10 +++++----- lib/facter/util/resolvers/ffi/hostname.rb | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/facter/resolvers/hostname.rb b/lib/facter/resolvers/hostname.rb index 53c7f9ac3c..c8fd7ec410 100644 --- a/lib/facter/resolvers/hostname.rb +++ b/lib/facter/resolvers/hostname.rb @@ -18,7 +18,7 @@ def post_resolve(fact_name, _options) def retrieve_info(fact_name) require 'socket' - output = Socket.gethostname + output = Socket.gethostname&.dup&.force_encoding(Encoding::UTF_8) || nil unless output log.debug('Socket.gethostname failed to return hostname') return @@ -55,7 +55,7 @@ def retrieve_with_addrinfo(host) end return if name.nil? || name.empty? || host == name[2] || name[2] == name[3] - name[2] + name[2]&.dup&.force_encoding(Encoding::UTF_8) end def exists_and_valid_fqdn?(fqdn, hostname) diff --git a/lib/facter/resolvers/linux/hostname.rb b/lib/facter/resolvers/linux/hostname.rb index 32e0688559..cba8a0c558 100644 --- a/lib/facter/resolvers/linux/hostname.rb +++ b/lib/facter/resolvers/linux/hostname.rb @@ -37,7 +37,7 @@ def retrieve_info(fact_name) end def retrieving_hostname - output = Socket.gethostname || '' + output = Socket.gethostname&.dup&.force_encoding(Encoding::UTF_8) || '' if output.empty? || output['0.0.0.0'] begin require_relative '../../../facter/util/resolvers/ffi/hostname' @@ -70,7 +70,7 @@ def retrieve_fqdn_for_host(host) log.debug("Socket.getaddrinfo failed to retrieve fqdn for hostname #{host} with: #{e}") end - return name[2] if !name.nil? && !name.empty? && host != name[2] && name[2] != name[3] + return name[2].dup.force_encoding(Encoding::UTF_8) if !name.nil? && !name.empty? && host != name[2] && name[2] != name[3] retrieve_fqdn_for_host_with_ffi(host) end diff --git a/lib/facter/util/linux/socket_parser.rb b/lib/facter/util/linux/socket_parser.rb index c7fd6b2ea5..442be40124 100644 --- a/lib/facter/util/linux/socket_parser.rb +++ b/lib/facter/util/linux/socket_parser.rb @@ -19,7 +19,7 @@ def retrieve_interfaces(logger) private def populate_interface_info(ifaddr) - interface_name = ifaddr.name + interface_name = ifaddr.name.dup.force_encoding(Encoding::UTF_8) @interfaces[interface_name] = {} if @interfaces[interface_name].nil? mac(ifaddr) @@ -80,7 +80,7 @@ def read_ip_link_show_data def mac_from(ifaddr) if Socket.const_defined? :PF_LINK - ifaddr.addr&.getnameinfo&.first # sometimes it returns localhost or ip + ifaddr.addr&.getnameinfo&.first&.dup&.force_encoding(Encoding::UTF_8) # sometimes it returns localhost or ip elsif Socket.const_defined? :PF_PACKET return if ifaddr.addr.nil? @@ -93,7 +93,7 @@ def mac_from(ifaddr) def mac_from_sockaddr_of(ifaddr) result = ifaddr.addr.inspect_sockaddr - result&.match(/hwaddr=([\h:]+)/)&.captures&.first + result&.match(/hwaddr=([\h:]+)/)&.captures&.first&.force_encoding(Encoding::UTF_8) end def ip_info_of(ifaddr) @@ -117,8 +117,8 @@ def add_binding(interface_name, ifaddr) def binding_data(ifaddr) # ipv6 ips are retrieved as % - ip = ifaddr.addr.ip_address.split('%').first - netmask = ifaddr.netmask.ip_address + ip = ifaddr.addr.ip_address.split('%').first.force_encoding(Encoding::UTF_8) + netmask = ifaddr.netmask.ip_address.dup.force_encoding(Encoding::UTF_8) binding_key = ifaddr.addr.ipv4? ? :bindings : :bindings6 [ip, netmask, binding_key] diff --git a/lib/facter/util/resolvers/ffi/hostname.rb b/lib/facter/util/resolvers/ffi/hostname.rb index 3a1a0b1804..55397c8891 100644 --- a/lib/facter/util/resolvers/ffi/hostname.rb +++ b/lib/facter/util/resolvers/ffi/hostname.rb @@ -33,7 +33,7 @@ def self.getffihostname res = Hostname.gethostname(raw_hostname, HOST_NAME_MAX) return unless res.zero? - raw_hostname.read_string + raw_hostname.read_string&.force_encoding(Encoding::UTF_8) end def self.getffiaddrinfo(hostname) @@ -54,7 +54,7 @@ def self.getffiaddrinfo(hostname) begin addr = Facter::Util::Resolvers::Ffi::AddrInfo.new(ret.read_pointer) - name = addr[:ai_canonname] + name = addr[:ai_canonname]&.dup&.force_encoding(Encoding::UTF_8) log.debug("FFI AddrInfo struct has the fqdn: #{name}") return if !name || hostname == name From 5e82c84e6929bc53a44a94d94139d65d2fedaac5 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 3 Apr 2025 22:38:58 -0700 Subject: [PATCH 15/29] Force rubysitedir to UTF-8 --- lib/facter/resolvers/ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/facter/resolvers/ruby.rb b/lib/facter/resolvers/ruby.rb index 094a6af630..4b3fb78b44 100644 --- a/lib/facter/resolvers/ruby.rb +++ b/lib/facter/resolvers/ruby.rb @@ -13,7 +13,7 @@ def post_resolve(fact_name, _options) end def retrieve_ruby_information(fact_name) - @fact_list[:sitedir] = RbConfig::CONFIG['sitelibdir'] if RbConfig::CONFIG['sitedir'] + @fact_list[:sitedir] = RbConfig::CONFIG['sitelibdir']&.dup&.force_encoding(Encoding::UTF_8) if RbConfig::CONFIG['sitedir'] @fact_list[:platform] = RUBY_PLATFORM @fact_list[:version] = RUBY_VERSION @fact_list[fact_name] From 74a441905b401118d5eb30bea7b04c4915c75f84 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 3 Apr 2025 22:39:10 -0700 Subject: [PATCH 16/29] Don't return binary string from custom fact ConnectServer returns a WIN32OLE object, whose to_s method returns a binary string. Since we're just verifying win32ole doesn't hang, return the class name from the custom fact, which is UTF-8 encoded. --- .../tests/custom_facts/using_win32ole_should_not_hang.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acceptance/tests/custom_facts/using_win32ole_should_not_hang.rb b/acceptance/tests/custom_facts/using_win32ole_should_not_hang.rb index 8e369cdd0b..08118272cb 100644 --- a/acceptance/tests/custom_facts/using_win32ole_should_not_hang.rb +++ b/acceptance/tests/custom_facts/using_win32ole_should_not_hang.rb @@ -9,7 +9,7 @@ setcode do require 'win32ole' locator = WIN32OLE.new('WbemScripting.SWbemLocator') - locator.ConnectServer('', "root/CIMV2", '', '', nil, nil, nil, nil).to_s + locator.ConnectServer('', "root/CIMV2", '', '', nil, nil, nil, nil).class.to_s end end EOM @@ -26,7 +26,7 @@ # Test is assumed to have hung if it takes longer than 5 seconds. Timeout::timeout(5) do on agent, facter('--custom-dir', custom_dir, 'custom_fact_ole') do |facter_result| - assert_match(/#/, facter_result.stdout.chomp, 'Custom fact output does not match expected output') + assert_match(/WIN32OLE/, facter_result.stdout.chomp, 'Custom fact output does not match expected output') end end end From ff435eda7acd187d30f6dbd052187bfc38eadd39 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 29 Apr 2025 10:10:00 -0700 Subject: [PATCH 17/29] Handle ENV variables on Windows Ruby < 3 Ruby < 3 returned ENV key/values based on the active code page, but the returned strings had ASCII-8BIT encoding. In Ruby 3.0, ENV key/values are UTF-8 encoded. See ruby/ruby@ca76337a00244635faa331afd04f4b75161ce6fb We should drop this special case when we drop Ruby 2.7 --- lib/facter/custom_facts/util/loader.rb | 17 +++++++++++++++-- lib/facter/resolvers/windows/system32.rb | 8 +++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/facter/custom_facts/util/loader.rb b/lib/facter/custom_facts/util/loader.rb index 0f72a91404..435127137c 100644 --- a/lib/facter/custom_facts/util/loader.rb +++ b/lib/facter/custom_facts/util/loader.rb @@ -115,6 +115,18 @@ def kernel_load(file) Kernel.load(file) end + # Ruby < 3 returned env key/values based on the active code page + # Ruby 3+ use UTF-8, see ruby/ruby@ca76337a00244635faa331afd04f4b75161ce6fb + if RUBY_VERSION.to_f < 3.0 + def env_string(str) + str.force_encoding(Encoding.default_external) + end + else + def env_string(str) + str + end + end + # Load facts from the environment. If no name is provided, # all will be loaded. def load_env(fact = nil) @@ -123,7 +135,8 @@ def load_env(fact = nil) # Skip anything that doesn't match our regex. next unless name =~ /^facter_?(\w+)$/i - env_name = Regexp.last_match(1).downcase + env_name = env_string(Regexp.last_match(1).downcase) + env_value = env_string(value.dup) # If a fact name was specified, skip anything that doesn't # match it. @@ -131,7 +144,7 @@ def load_env(fact = nil) LegacyFacter.add(env_name, fact_type: :external, is_env: true) do has_weight 1_000_000 - setcode { value } + setcode { env_value } end # Short-cut, if we are only looking for one value. diff --git a/lib/facter/resolvers/windows/system32.rb b/lib/facter/resolvers/windows/system32.rb index d627c37f53..620a9ecb25 100644 --- a/lib/facter/resolvers/windows/system32.rb +++ b/lib/facter/resolvers/windows/system32.rb @@ -15,7 +15,13 @@ def post_resolve(fact_name, _options) def retrieve_windows_binaries_path require_relative '../../../facter/resolvers/windows/ffi/system32_ffi' - windows_path = ENV['SystemRoot'] + # Ruby < 3 returned env key/values based on the active code page + # Ruby 3+ use UTF-8, see ruby/ruby@ca76337a00244635faa331afd04f4b75161ce6fb + windows_path = if RUBY_VERSION.to_f < 3.0 + ENV['SystemRoot']&.dup&.force_encoding(Encoding.default_external) + else + ENV['SystemRoot'] + end if !windows_path || windows_path.empty? @log.debug 'Unable to find correct value for SystemRoot enviroment variable' From 34a8782e1d23a3e0b72d3971e2b4e97ec41b8749 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 1 May 2025 16:29:48 -0700 Subject: [PATCH 18/29] Validate resolved facts If fact contains a binary string, a warning will be printed: Fact 'networking.interfaces' contains an invalid value: String 'en0,lo' contains binary data If --trace is specified, then a backtrace is printed too --- lib/facter/custom_facts/util/normalization.rb | 4 ++++ lib/facter/framework/core/fact_manager.rb | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/facter/custom_facts/util/normalization.rb b/lib/facter/custom_facts/util/normalization.rb index b768f42987..5ef218a86f 100644 --- a/lib/facter/custom_facts/util/normalization.rb +++ b/lib/facter/custom_facts/util/normalization.rb @@ -48,6 +48,10 @@ def normalize(value) # @return [void] def normalize_string(value) + if value.encoding == Encoding::ASCII_8BIT + raise NormalizationError, "String #{value.inspect} contains binary data" + end + value = value.encode(Encoding::UTF_8) unless value.valid_encoding? diff --git a/lib/facter/framework/core/fact_manager.rb b/lib/facter/framework/core/fact_manager.rb index ccd735c29a..e271c5ea0a 100644 --- a/lib/facter/framework/core/fact_manager.rb +++ b/lib/facter/framework/core/fact_manager.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true +require_relative '../../custom_facts/util/normalization' + module Facter class FactManager include Singleton + include LegacyFacter::Util::Normalization def initialize @internal_fact_mgr = InternalFactManager.new @@ -30,6 +33,8 @@ def resolve_facts(user_query = []) FactFilter.new.filter_facts!(resolved_facts, user_query) + validate_resolved_facts(resolved_facts) + log_resolved_facts(resolved_facts) resolved_facts end @@ -57,6 +62,8 @@ def resolve_fact(user_query) @cache_manager.cache_facts(resolved_facts) + validate_resolved_facts(resolved_facts) + log_resolved_facts(resolved_facts) resolved_facts end @@ -64,11 +71,23 @@ def resolve_fact(user_query) def resolve_core(user_query = [], options = {}) log_resolving_method @cache_manager = CacheManager.new - core_fact(user_query, options) + resolved_facts = core_fact(user_query, options) + validate_resolved_facts(resolved_facts) + resolved_facts end private + def validate_resolved_facts(resolved_facts) + resolved_facts.each do |fact| + normalize(fact.value) + rescue LegacyFacter::Util::Normalization::NormalizationError => e + detail = e.message + detail += "\n#{e.backtrace.join("\n")}" if @options[:trace] + @log.warn("Fact '#{fact.name}' contains an invalid value: #{detail}") + end + end + def log_resolving_method if Options[:sequential] @log.debugonce('Resolving facts sequentially') From 160ecbd8b833eae83dc6652ec4d4bfb38a2bab56 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 2 May 2025 09:25:06 -0700 Subject: [PATCH 19/29] Clear more global state If one test caused Facter, FactLoader or FactManager to lazily inititalize a logger using an instance double, then that state leaked into later tests. If a later test tried to call the logger, rspec reported ``` 1) LegacyFacter::Util::Loader when loading all facts when loads all facts from the environment loaded fact two Failure/Error: @log.debugonce('Resolving facts sequentially') # was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for. # ./lib/facter/framework/core/fact_manager.rb:93:in `log_resolving_method' # ./lib/facter/framework/core/fact_manager.rb:73:in `resolve_core' # ./lib/facter.rb:134:in `core_value' ``` For now, clear the global logger state after each test. --- spec/spec_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1b68195b42..3e63323dac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -89,6 +89,9 @@ Facter::OptionStore.reset Facter::ConfigReader.clear Facter::ConfigFileOptions.clear + Facter.instance_variable_set(:@logger, nil) + Facter::FactLoader.instance.instance_variable_set(:@logger, nil) + Facter::FactManager.instance.instance_variable_set(:@logger, nil) end end From 6f128815d8d95bcac7f3fbe0626a4ae44dd319c9 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 2 May 2025 10:08:48 -0700 Subject: [PATCH 20/29] Force AIX networking facts to UTF-8 These were set to binary: en0.ip en0.netmask en0.network lo0.ip6 lo0.netmask6 lo0.network6 scope6 & mac are already UTF-8 --- lib/facter/resolvers/aix/ffi/ffi_helper.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/facter/resolvers/aix/ffi/ffi_helper.rb b/lib/facter/resolvers/aix/ffi/ffi_helper.rb index c1c6535e75..169fb9da6b 100644 --- a/lib/facter/resolvers/aix/ffi/ffi_helper.rb +++ b/lib/facter/resolvers/aix/ffi/ffi_helper.rb @@ -59,7 +59,7 @@ def self.read_interfaces when FFI::RTM_IFINFO link_addr = FFI::SockaddrDl.new(hdr.to_ptr + hdr.size) - interface_name = link_addr[:sdl_data].to_s[0, link_addr[:sdl_nlen]] + interface_name = link_addr[:sdl_data].to_s[0, link_addr[:sdl_nlen]].dup.force_encoding(Encoding::UTF_8) interfaces[interface_name] ||= {} when FFI::RTM_NEWADDR @@ -101,9 +101,9 @@ def self.read_interfaces bindings = family == FFI::AF_INET ? :bindings : :bindings6 interfaces[interface_name][bindings] ||= [] interfaces[interface_name][bindings] << { - netmask: netmask.read_string, - address: address.read_string, - network: network.read_string + netmask: netmask, + address: address, + network: network } else log.debug("got an unknown RT_IFLIST message: #{hdr[:ifm_type]}") @@ -135,7 +135,7 @@ def self.address_to_string(sockaddr, mask = nil) buffer = ::FFI::MemoryPointer.new(:char, FFI::INET_ADDRSTRLEN) Libc.inet_ntop(FFI::AF_INET, in_addr_ip[:sin_addr].to_ptr, buffer, 16) - buffer + buffer.read_string.dup.force_encoding(Encoding::UTF_8) elsif sockaddr[:sa_family] == FFI::AF_INET6 in_addr_ip = FFI::SockaddrIn6.new(sockaddr.to_ptr) if mask && sockaddr[:sa_family] == mask[:sa_family] @@ -148,7 +148,7 @@ def self.address_to_string(sockaddr, mask = nil) buffer = ::FFI::MemoryPointer.new(:char, FFI::INET6_ADDRSTRLEN) Libc.inet_ntop(FFI::AF_INET6, in_addr_ip[:sin6_addr].to_ptr, buffer, 16) - buffer + buffer.read_string.dup.force_encoding(Encoding::UTF_8) end end From 705b4ac079247592acacb0563e20a18c3c41f9fb Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 5 May 2025 11:07:11 -0700 Subject: [PATCH 21/29] Force Solaris networking facts to UTF-8 These were set to binary: lo0.ip lo0.ip6 net0.ip net0.ip6 --- lib/facter/resolvers/solaris/ffi/structs.rb | 2 +- lib/facter/resolvers/solaris/networking.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/facter/resolvers/solaris/ffi/structs.rb b/lib/facter/resolvers/solaris/ffi/structs.rb index 0e73f27252..86cf9b1478 100644 --- a/lib/facter/resolvers/solaris/ffi/structs.rb +++ b/lib/facter/resolvers/solaris/ffi/structs.rb @@ -64,7 +64,7 @@ class Lifreq < ::FFI::Struct :pad, [:char, 80] def name - self[:lifr_name].to_s + self[:lifr_name].to_s.dup.force_encoding(Encoding::UTF_8) end def ss_family diff --git a/lib/facter/resolvers/solaris/networking.rb b/lib/facter/resolvers/solaris/networking.rb index ad6e1c1293..08e64fe389 100644 --- a/lib/facter/resolvers/solaris/networking.rb +++ b/lib/facter/resolvers/solaris/networking.rb @@ -110,7 +110,8 @@ def inet_ntop(lifreq, ss_family) buffer = ::FFI::MemoryPointer.new(:char, buffer_size) - FFI::Ioctl.inet_ntop(ss_family, ip.to_ptr, buffer.to_ptr, buffer.size) + str = FFI::Ioctl.inet_ntop(ss_family, ip.to_ptr, buffer.to_ptr, buffer.size) + str.dup.force_encoding(Encoding::UTF_8) end def get_ipv4(lifreq) From 555da78f2e49acd3539348c76cbacce83ffd5ee3 Mon Sep 17 00:00:00 2001 From: skyamgarp <130442619+skyamgarp@users.noreply.github.com> Date: Fri, 30 May 2025 15:27:18 +0530 Subject: [PATCH 22/29] (PA-7511) Fix test failures with os.description,processor.isa and networking fact Redhat 10 release string looks like: NAME="Red Hat Enterprise Linux" VERSION="10.0 (Coughlan)" ID="rhel" ID_LIKE="centos fedora" VERSION_ID="10.0" PLATFORM_ID="platform:el10" PRETTY_NAME="Red Hat Enterprise Linux 10.0 (Coughlan)" which passes centos condition, hence added one more condition with rhel --- acceptance/lib/facter/acceptance/base_fact_utils.rb | 10 ++++++++-- acceptance/tests/facts/networking_facts.rb | 2 +- lib/facter/custom_facts/core/suitable.rb | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/acceptance/lib/facter/acceptance/base_fact_utils.rb b/acceptance/lib/facter/acceptance/base_fact_utils.rb index 9542fe1871..281bcadfcd 100644 --- a/acceptance/lib/facter/acceptance/base_fact_utils.rb +++ b/acceptance/lib/facter/acceptance/base_fact_utils.rb @@ -118,7 +118,7 @@ def debian_expected_facts(agent) # el (RedHat, Centos) def el_expected_facts(agent) - version = agent['platform'].match(/(el|centos)-(\d)/) + version = agent['platform'].match(/(el|centos)-(\d+)/) os_version = if version.nil? /\d+/ else @@ -143,6 +143,11 @@ def el_expected_facts(agent) os_distro_description = /Rocky Linux release #{os_version}\.\d+ \(.+\)/ os_distro_id = 'Rocky' os_distro_release_full = /#{os_version}\.\d+/ + when /rhel/ + os_name = 'RedHat' + os_distro_description = /Red Hat Enterprise Linux( Server)? release #{os_version}\.\d+( Beta)? \(\w+\)/ + os_distro_id = /^RedHatEnterprise(Server)?$/ + os_distro_release_full = /#{os_version}\.\d+/ when /centos/ os_name = 'CentOS' os_distro_description = /CentOS( Linux)? release #{os_version}\.\d+(\.\d+)? \(\w+\)/ @@ -154,6 +159,7 @@ def el_expected_facts(agent) os_distro_id = /^RedHatEnterprise(Server)?$/ os_distro_release_full = /#{os_version}\.\d+/ end + if agent['platform'] =~ /x86_64/ os_arch = 'x86_64' os_hardware = 'x86_64' @@ -190,7 +196,7 @@ def el_expected_facts(agent) expected_facts['os.release.minor'] = /(\d+)?/ expected_facts['processors.count'] = /[1-9]/ expected_facts['processors.physicalcount'] = /[1-9]/ - expected_facts['processors.isa'] = os_hardware + expected_facts['processors.isa'] = /unknown|#{os_hardware}/ expected_facts['processors.models'] = processor_model_pattern expected_facts['kernel'] = 'Linux' expected_facts['kernelrelease'] = /\d+\.\d+\.\d+/ diff --git a/acceptance/tests/facts/networking_facts.rb b/acceptance/tests/facts/networking_facts.rb index 64f7077316..b846ef7171 100644 --- a/acceptance/tests/facts/networking_facts.rb +++ b/acceptance/tests/facts/networking_facts.rb @@ -11,7 +11,7 @@ agents.each do |agent| expected_networking = { - %w[networking dhcp] => agent['platform'] =~ /fedora-|el-8-|el-9-/ ? '' : @ip_regex, # https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/426 + %w[networking dhcp] => agent['platform'] =~ /fedora-|el-(8|9|10)/ ? '' : @ip_regex, # https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/426 %w[networking ip] => @ip_regex, %w[networking ip6] => /[a-f0-9]+:+/, %w[networking mac] => /[a-f0-9]{2}:/, diff --git a/lib/facter/custom_facts/core/suitable.rb b/lib/facter/custom_facts/core/suitable.rb index 9e9af4b3cd..1503cfa945 100644 --- a/lib/facter/custom_facts/core/suitable.rb +++ b/lib/facter/custom_facts/core/suitable.rb @@ -18,7 +18,7 @@ module Suitable # @return [void] # # @api public - def has_weight(weight) # rubocop:disable Naming/PredicateName + def has_weight(weight) # rubocop:disable Naming/PredicatePrefix @weight = weight self end From 8917a468e65bfc335bf34e9ecf22121be176a926 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 2 Jul 2025 22:05:25 -0700 Subject: [PATCH 23/29] Include the help header in the gem The built gem didn't include the help header fixture. Just inline the text. --- .rubocop.yml | 1 + lib/facter/fixtures/facter_help_header | 7 ------- lib/facter/framework/cli/cli.rb | 16 +++++++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 lib/facter/fixtures/facter_help_header diff --git a/.rubocop.yml b/.rubocop.yml index 4b924cd515..50565e9b59 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -47,6 +47,7 @@ Naming/MethodName: Exclude: - 'spec/mocks/win32ole.rb' - 'spec/mocks/ffi.rb' + - 'spec/facter/util/windows/network_utils_spec.rb' Naming/PredicateName: Exclude: diff --git a/lib/facter/fixtures/facter_help_header b/lib/facter/fixtures/facter_help_header deleted file mode 100644 index 5a17763c8d..0000000000 --- a/lib/facter/fixtures/facter_help_header +++ /dev/null @@ -1,7 +0,0 @@ -Usage -===== - - facter [options] [query] [query] [...] - -Options -======= diff --git a/lib/facter/framework/cli/cli.rb b/lib/facter/framework/cli/cli.rb index 55d9c13c19..0ca3fa7858 100755 --- a/lib/facter/framework/cli/cli.rb +++ b/lib/facter/framework/cli/cli.rb @@ -167,9 +167,9 @@ def list_cache_groups end desc '--help, -h', 'Help for all arguments' - def help(*args) + def help(*_args) help_string = +'' - help_string << help_header(args) + help_string << help_header help_string << add_class_options_to_help help_string << add_commands_to_help @@ -177,10 +177,16 @@ def help(*args) end no_commands do - def help_header(_args) - path = File.join(File.dirname(__FILE__), '../../') + def help_header + <<~HELP + Usage + ===== - Facter::Util::FileHelper.safe_read("#{path}fixtures/facter_help_header") + facter [options] [query] [query] [...] + + Options + ======= + HELP end def add_class_options_to_help From d0db9334fb5471b126c064356b0647878565a0ea Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 2 Jul 2025 21:59:38 -0700 Subject: [PATCH 24/29] Modify files in facter gem This commit adds the following files to the built gem. It also removes directories from the list of "files" to include in the gemspec. ``` $ gem fetch facter --version 4.13.0 $ gem build facter.gemspec $ diff \ <(tar xOf facter-4.13.0.gem data.tar.gz | tar zt | sort) \ <(tar xOf facter-4.14.0.gem data.tar.gz | tar zt | sort) 1a2,3 > facter.gemspec > Gemfile 949a953 > README.md ``` This reverts absolute path dir globbing added in commit 61564e26900. That issue will be fixed in PA-7583. --- facter.gemspec | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/facter.gemspec b/facter.gemspec index 74ed30af94..956b1dad37 100644 --- a/facter.gemspec +++ b/facter.gemspec @@ -14,15 +14,9 @@ Gem::Specification.new do |spec| spec.description = 'You can prove anything with facts!' spec.license = 'Apache-2.0' - dirs = - Dir[File.join(__dir__, 'bin/facter')] + - Dir[File.join(__dir__, 'LICENSE')] + - Dir[File.join(__dir__, 'lib/**/*.rb')] + - Dir[File.join(__dir__, 'lib/**/*.json')] + - Dir[File.join(__dir__, 'lib/**/*.conf')] + - Dir[File.join(__dir__, 'lib/**/*.erb')] - base = "#{__dir__}#{File::SEPARATOR}" - spec.files = dirs.map { |path| path.sub(base, '') } + spec.files = + %w[LICENSE README.md Gemfile facter.gemspec] + + Dir['{bin,lib}/**/*'].reject { |f| File.directory?(f) || f.end_with?('.dox') || f.end_with?('.yaml') } spec.required_ruby_version = '>= 2.5', '< 4.0' spec.bindir = 'bin' From 56f48415ba04a3cde204c055216f6fbd5df56cbf Mon Sep 17 00:00:00 2001 From: Aria Li Date: Wed, 2 Jul 2025 15:13:25 -0700 Subject: [PATCH 25/29] (FACT-3497) Facter should not fail when binary data is detected This commit updates LegacyFacter::Util::Normalization.normalize_string to accept fact values with ASCII 8-BIT without immediately failing. When a fact value with ASCII 8-BIT is detected but can be force encoded to UTF-8, Facter will log a debug message with the fact name and value. Facter will only error (with the fact name & value) when the fact value cannot be successfully force encoded to UTF-8. This commit also updates all the normalize methods in LegacyFacter::Util::Normalization to take in 2 parameters now: the fact name & value so the debug and error messages can include the fact name. This helps users more easily figure out which fact is causing issues and has binary. --- lib/facter/custom_facts/core/resolvable.rb | 2 +- lib/facter/custom_facts/util/fact.rb | 2 +- lib/facter/custom_facts/util/normalization.rb | 33 +++++++----- lib/facter/framework/core/fact_manager.rb | 2 +- spec/custom_facts/util/normalization_spec.rb | 51 ++++++++++++------- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/lib/facter/custom_facts/core/resolvable.rb b/lib/facter/custom_facts/core/resolvable.rb index a3be67aa9b..1b476853a4 100644 --- a/lib/facter/custom_facts/core/resolvable.rb +++ b/lib/facter/custom_facts/core/resolvable.rb @@ -79,7 +79,7 @@ def value end end - LegacyFacter::Util::Normalization.normalize(result) + LegacyFacter::Util::Normalization.normalize(@fact.name, result) rescue Timeout::Error => e Facter.log_exception(e, "Timed out after #{limit} seconds while resolving #{qualified_name}") diff --git a/lib/facter/custom_facts/util/fact.rb b/lib/facter/custom_facts/util/fact.rb index 863b79defc..02f307c4b3 100644 --- a/lib/facter/custom_facts/util/fact.rb +++ b/lib/facter/custom_facts/util/fact.rb @@ -35,7 +35,7 @@ class Fact # # @api private def initialize(name, options = {}) - @name = LegacyFacter::Util::Normalization.normalize(name.to_s.downcase).intern + @name = LegacyFacter::Util::Normalization.normalize(name.to_s.downcase, name.to_s.downcase).intern @options = options.dup extract_ldapname_option!(options) diff --git a/lib/facter/custom_facts/util/normalization.rb b/lib/facter/custom_facts/util/normalization.rb index 5ef218a86f..ea8f95ef03 100644 --- a/lib/facter/custom_facts/util/normalization.rb +++ b/lib/facter/custom_facts/util/normalization.rb @@ -17,18 +17,18 @@ class NormalizationError < StandardError; end # @api public # @raise [NormalizationError] If the data structure contained an invalid element. # @return [void] - def normalize(value) + def normalize(fact_name, value) case value when Integer, Float, TrueClass, FalseClass, NilClass, Symbol value when Date, Time value.iso8601 when String - normalize_string(value) + normalize_string(fact_name, value) when Array - normalize_array(value) + normalize_array(fact_name, value) when Hash - normalize_hash(value) + normalize_hash(fact_name, value) else raise NormalizationError, "Expected #{value} to be one of #{VALID_TYPES.inspect}, but was #{value.class}" end @@ -47,12 +47,17 @@ def normalize(value) # @param value [String] # @return [void] - def normalize_string(value) + def normalize_string(fact_name, value) if value.encoding == Encoding::ASCII_8BIT - raise NormalizationError, "String #{value.inspect} contains binary data" - end + value_copy_encoding = value.dup.force_encoding(Encoding::UTF_8) + raise NormalizationError, "custom fact \"#{fact_name}\" with value: #{value_copy_encoding} contains binary data and could not be force encoded to UTF-8" unless value_copy_encoding.valid_encoding? + + log.debug("custom fact \"#{fact_name}\" with value: #{value} contained binary data and was force encoded to UTF-8 and now has the value: #{value_copy_encoding}") + value = value_copy_encoding - value = value.encode(Encoding::UTF_8) + else + value = value.encode(Encoding::UTF_8) + end unless value.valid_encoding? raise NormalizationError, "String #{value.inspect} doesn't match the reported encoding #{value.encoding}" @@ -73,9 +78,9 @@ def normalize_string(value) # @raise [NormalizationError] If one of the elements failed validation # @param value [Array] # @return [void] - def normalize_array(value) + def normalize_array(fact_name, value) value.collect do |elem| - normalize(elem) + normalize(fact_name, elem) end end @@ -85,8 +90,12 @@ def normalize_array(value) # @raise [NormalizationError] If one of the keys or values failed normalization # @param value [Hash] # @return [void] - def normalize_hash(value) - Hash[value.collect { |k, v| [normalize(k), normalize(v)] }] + def normalize_hash(fact_name, value) + Hash[value.collect { |k, v| [normalize(fact_name, k), normalize(fact_name, v)] }] + end + + def log + Facter::Log.new(self) end end end diff --git a/lib/facter/framework/core/fact_manager.rb b/lib/facter/framework/core/fact_manager.rb index e271c5ea0a..acee18f4dd 100644 --- a/lib/facter/framework/core/fact_manager.rb +++ b/lib/facter/framework/core/fact_manager.rb @@ -80,7 +80,7 @@ def resolve_core(user_query = [], options = {}) def validate_resolved_facts(resolved_facts) resolved_facts.each do |fact| - normalize(fact.value) + normalize(fact.name, fact.value) rescue LegacyFacter::Util::Normalization::NormalizationError => e detail = e.message detail += "\n#{e.backtrace.join("\n")}" if @options[:trace] diff --git a/spec/custom_facts/util/normalization_spec.rb b/spec/custom_facts/util/normalization_spec.rb index 62cb4437c4..eaabd7eaaf 100644 --- a/spec/custom_facts/util/normalization_spec.rb +++ b/spec/custom_facts/util/normalization_spec.rb @@ -23,32 +23,49 @@ def utf8(str) describe 'and string encoding is supported', if: String.instance_methods.include?(:encoding) do it 'accepts strings that are ASCII and match their encoding and converts them to UTF-8' do str = 'ASCII'.encode(Encoding::ASCII) - normalized_str = normalization.normalize(str) + normalized_str = normalization.normalize('', str) expect(normalized_str.encoding).to eq(Encoding::UTF_8) end + it 'accepts ASCII 8-BIT (binary) string that can be successfully force encoded to UTF-8 and logs a debug message with the fact name and value and returns UTF-8 encoded version of the passed in string' do + str = 'test'.encode(Encoding::ASCII_8BIT) + logger = Facter::Log.new(self) + allow(normalization).to receive(:log).and_return logger # rubocop:disable RSpec/SubjectStub + expect(logger).to receive(:debug).with(/custom fact "" with value: #{str} contained binary data and was force encoded to UTF-8 and now has the value: #{str}/) + value = normalization.normalize('', str) + expect(value).to eq('test') + expect(value.encoding).to eq(Encoding::UTF_8) + end + + it 'errors when given binary string that cannot be successfully force encoded to UTF-8' do + str = "\xc0".dup.force_encoding(Encoding::ASCII_8BIT) # rubocop:disable Performance/UnfreezeString + expect do + normalization.normalize('', str) + end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError) + end + it 'accepts strings that are UTF-8 and match their encoding' do str = "let's make a ☃!".encode(Encoding::UTF_8) - expect(normalization.normalize(str)).to eq(str) + expect(normalization.normalize('', str)).to eq(str) end it 'converts valid non UTF-8 strings to UTF-8' do str = "let's make a ☃!".encode(Encoding::UTF_16LE) - enc = normalization.normalize(str).encoding + enc = normalization.normalize('', str).encoding expect(enc).to eq(Encoding::UTF_8) end it 'normalizes a frozen string returning a non-frozen string' do str = 'factvalue'.encode(Encoding::UTF_16LE).freeze - normalized_str = normalization.normalize(str) + normalized_str = normalization.normalize('', str) expect(normalized_str).not_to be_frozen end it 'rejects strings that are not UTF-8 and do not match their claimed encoding' do invalid_shift_jis = (+"\xFF\x5C!").force_encoding(Encoding::SHIFT_JIS) expect do - normalization.normalize(invalid_shift_jis) + normalization.normalize('', invalid_shift_jis) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String encoding Shift_JIS is not UTF-8 and could not be converted to UTF-8/) end @@ -56,7 +73,7 @@ def utf8(str) it "rejects strings that claim to be UTF-8 encoded but aren't" do str = (+"\255ay!").force_encoding(Encoding::UTF_8) expect do - normalization.normalize(str) + normalization.normalize('', str) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String "\\xADay!" doesn't match the reported encoding UTF-8/) end @@ -64,7 +81,7 @@ def utf8(str) it 'rejects strings that only have the start of a valid UTF-8 sequence' do str = "\xc3\x28" expect do - normalization.normalize(str) + normalization.normalize('', str) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String "\\xC3\(" doesn't match the reported encoding UTF-8/) end @@ -73,7 +90,7 @@ def utf8(str) str = 'Mitteleuropäische Zeit'.encode(Encoding::UTF_16LE) str.force_encoding(Encoding::UTF_8) expect do - normalization.normalize(str) + normalization.normalize('', str) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String.*doesn't match the reported encoding UTF-8/) end @@ -85,7 +102,7 @@ def utf8(str) "null byte \x00 in the middle"] test_strings.each do |str| expect do - normalization.normalize(str) + normalization.normalize('', str) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String.*contains a null byte reference/) end @@ -95,13 +112,13 @@ def utf8(str) describe 'and string encoding is not supported', unless: String.instance_methods.include?(:encoding) do it 'accepts strings that are UTF-8 and match their encoding' do str = "let's make a ☃!" - expect(normalization.normalize(str)).to eq(str) + expect(normalization.normalize('', str)).to eq(str) end it 'rejects strings that are not UTF-8' do str = "let's make a \255\255\255!" expect do - normalization.normalize(str) + normalization.normalize('', str) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /String .* is not valid UTF-8/) end end @@ -112,7 +129,7 @@ def utf8(str) arr = [utf16('first'), utf16('second'), [utf16('third'), utf16('fourth')]] expected_arr = [utf8('first'), utf8('second'), [utf8('third'), utf8('fourth')]] - expect(normalization.normalize_array(arr)).to eq(expected_arr) + expect(normalization.normalize_array('', arr)).to eq(expected_arr) end end @@ -121,7 +138,7 @@ def utf8(str) hsh = { utf16('first') => utf16('second'), utf16('third') => [utf16('fourth'), utf16('fifth')] } expected_hsh = { utf8('first') => utf8('second'), utf8('third') => [utf8('fourth'), utf8('fifth')] } - expect(normalization.normalize_hash(hsh)).to eq(expected_hsh) + expect(normalization.normalize_hash('', hsh)).to eq(expected_hsh) end end @@ -131,7 +148,7 @@ def utf8(str) value = Time.utc(2020) expected = '2020-01-01T00:00:00Z' - expect(normalization.normalize(value)).to eq(expected) + expect(normalization.normalize('', value)).to eq(expected) end end @@ -140,20 +157,20 @@ def utf8(str) value = Date.new(2020) expected = '2020-01-01' - expect(normalization.normalize(value)).to eq(expected) + expect(normalization.normalize('', value)).to eq(expected) end end [1, 1.0, true, false, nil, :symbol].each do |val| it "accepts #{val.inspect}:#{val.class}" do - expect(normalization.normalize(val)).to eq(val) + expect(normalization.normalize('', val)).to eq(val) end end [Object.new, Set.new].each do |val| it "rejects #{val.inspect}:#{val.class}" do expect do - normalization.normalize(val) + normalization.normalize('', val) end.to raise_error(LegacyFacter::Util::Normalization::NormalizationError, /Expected .*but was #{val.class}/) end end From b6016423107cfd2b0c73c3c2adeb9c21deb270ea Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 27 Jan 2025 15:45:59 -0800 Subject: [PATCH 26/29] (FACT-3488) Ignore unknown container types Prior to commit 7bc38ccb58e6abf66d6141a3b2e9f60eb0077d71, facter checked for docker and lxc in `/proc/1/cgroups` and fell back to the `container` environment variable in `/proc/1/environ`. The above commit reversed the order and labeled unknown container types as 'container_other'. This broke the VirtualDetecter, because it calls the `containers` resolver and assumes it can fallback to other methods like check_freebsd, etc https://github.com/puppetlabs/facter/blob/0656d9a34ce4790129f1fd6eba5bb4d49a9b9ad1/lib/facter/util/facts/posix/virtual_detector.rb#L11 The regression was noticed and corrected for Illumox LX in commit 31d32da513aae094bdbcc011155c2e247be11284, but we didn't realize other forms of virtualization were affected. This commit restores the original behavior of checking `/proc/1/cgroups` first, then `/proc/1/environ`. If a container type is not recognized, we log a debug message and continue, so that VirtualDetector works as expected. --- lib/facter/resolvers/containers.rb | 47 +++++++++++------------- spec/facter/resolvers/containers_spec.rb | 6 +-- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/facter/resolvers/containers.rb b/lib/facter/resolvers/containers.rb index 3726509e6f..4a468c7611 100644 --- a/lib/facter/resolvers/containers.rb +++ b/lib/facter/resolvers/containers.rb @@ -3,23 +3,29 @@ module Facter module Resolvers class Containers < BaseResolver - # :virtual + # :vm # :hypervisor init_resolver - INFO = { 'docker' => 'id', 'lxc' => 'name' }.freeze - class << self private def post_resolve(fact_name, _options) @fact_list.fetch(fact_name) do - read_environ(fact_name) || read_cgroup(fact_name) + vm, hypervisor = read_cgroup || read_environ + + @fact_list[:vm] = vm + @fact_list[:hypervisor] = hypervisor + @fact_list[fact_name] end end - def read_cgroup(fact_name) + # Read /proc/1/cgroup to determine if we're running in docker or lxc + # returning the vm and hypervisor info. If none found, return nil. + # + # @return Array[], nil + def read_cgroup output_cgroup = Facter::Util::FileHelper.safe_read('/proc/1/cgroup', nil) return unless output_cgroup @@ -41,12 +47,15 @@ def read_cgroup(fact_name) # fallback to read_environ return nil end - @fact_list[:vm] = vm - @fact_list[:hypervisor] = { vm.to_sym => info } if vm - @fact_list[fact_name] + + [vm, { vm.to_sym => info }] end - def read_environ(fact_name) + # Read the `container` environment variable from /proc/1/environ to + # determine the vm and hypervisor info. If none found, return nil. + # + # @return Array[], nil + def read_environ begin container = Facter::Util::Linux::Proc.getenv_for_pid(1, 'container') rescue StandardError => e @@ -76,25 +85,11 @@ def read_environ(fact_name) {} end else - vm = 'container_other' - log.warn("Container runtime, '#{container}', is unsupported, setting to '#{vm}'") - end - @fact_list[:vm] = vm - @fact_list[:hypervisor] = { vm.to_sym => info } if vm - @fact_list[fact_name] - end - - def extract_vm_and_info(output_docker, output_lxc) - vm = nil - if output_docker - vm = 'docker' - info = output_docker[1] - elsif output_lxc - vm = 'lxc' - info = output_lxc[1] + log.debug("Container runtime '#{container}' is not recognized, ignoring") + return nil end - [info ? { INFO[vm] => info } : {}, vm] + [vm, { vm.to_sym => info }] end end end diff --git a/spec/facter/resolvers/containers_spec.rb b/spec/facter/resolvers/containers_spec.rb index 34259b1c20..afa7507ddf 100644 --- a/spec/facter/resolvers/containers_spec.rb +++ b/spec/facter/resolvers/containers_spec.rb @@ -146,11 +146,11 @@ context 'when hypervisor is an unknown container runtime discovered by environ' do let(:cgroup_output) { load_fixture('cgroup_file').read } let(:environ_output) { ['container=UNKNOWN'] } - let(:logger) { Facter::Log.class_variable_get(:@@logger) } + let(:logger) { containers_resolver.log } it 'return container_other for vm' do - expect(logger).to receive(:warn).with(/Container runtime, 'UNKNOWN', is unsupported, setting to 'container_other'/) - expect(containers_resolver.resolve(:vm)).to eq('container_other') + expect(logger).to receive(:debug).with(/Container runtime 'UNKNOWN' is not recognized, ignoring/) + expect(containers_resolver.resolve(:vm)).to eq(nil) end end end From e583ea26cabb47b247f2b4a31785b76342ba9bbd Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 28 Apr 2025 11:49:03 -0700 Subject: [PATCH 27/29] Delete GH acceptance tests The acceptance_tests workflow runs the facter acceptance tests against the last passing nightly puppet-agent build. While we could test against internal nightly builds, there are some obstacles: * Twingate doesn't support mac * Our internal nightly builds don't understand "latest" for windows builds, so the GH action would need to look the latest version up and request it * We're already testing facter acceptance tests in nightly CI on all platforms * The Ubuntu-20.04 GH runner was retired. To get the tests working on 24.04, we'd need to install 'apt install isc-dhcp-client' for dhclient. Without that package the dhcp related network facts will be omitted, causing 'tests/facts/networking_facts.rb' to fail. So for now, remove this workflow. --- .github/actions/presuite.rb | 157 ------------------------- .github/workflows/acceptance_tests.yml | 67 ----------- 2 files changed, 224 deletions(-) delete mode 100644 .github/actions/presuite.rb delete mode 100644 .github/workflows/acceptance_tests.yml diff --git a/.github/actions/presuite.rb b/.github/actions/presuite.rb deleted file mode 100644 index f0c943146e..0000000000 --- a/.github/actions/presuite.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -require 'open3' -require 'fileutils' - -def if_no_env_vars_set_defaults - ENV['FACTER_ROOT'] = __dir__.gsub('/.github/actions', '') unless ENV['FACTER_ROOT'] - ENV['SHA'] = 'latest' unless ENV['SHA'] - ENV['RELEASE_STREAM'] = 'puppet7' unless ENV['RELEASE_STREAM'] -end - -def install_bundler - message('INSTALL BUNDLER') - run('gem install bundler') -end - -def install_facter_acceptance_dependencies - message('INSTALL FACTER ACCEPTANCE DEPENDENCIES') - run('bundle install') -end - -def initialize_beaker - beaker_platform_with_options = platform_with_options(beaker_platform) - - message('BEAKER INITIALIZE') - run("beaker init -h #{beaker_platform_with_options} -o #{File.join('config', 'aio', 'options.rb')}") - - message('BEAKER PROVISION') - run('beaker provision') -end - -def beaker_platform - { - 'ubuntu-20.04' => 'ubuntu2004-64a', - 'macos-12' => 'osx12-64a', - 'windows-2016' => 'windows2016-64a', - 'windows-2019' => 'windows2019-64a' - }[HOST_PLATFORM] -end - -def platform_with_options(platform) - return "\"#{platform}{hypervisor=none,hostname=localhost,is_cygwin=false}\"" if platform.include? 'windows' - - "#{platform}{hypervisor=none\\,hostname=localhost}" -end - -def install_puppet_agent - message('INSTALL PUPPET AGENT') - - beaker_puppet_root = run('bundle info beaker-puppet --path') - - # Bundler/Rubygems can sometimes give output other than the filepath (deprecation warnings, etc.) - begin - if File.exist?(beaker_puppet_root.chomp) - presuite_file_path = File.join(beaker_puppet_root.chomp, 'setup', 'aio', '010_Install_Puppet_Agent.rb') - - run("beaker exec pre-suite --pre-suite #{presuite_file_path} --preserve-state", './', env_path_var) - else - exit - end - rescue SystemExit - puts "`bundle info beaker-puppet --path` produced unexpected output, please address this." - end -end - -def puppet_puppet_bin_dir - return '/opt/puppetlabs/puppet/bin' unless HOST_PLATFORM.include? 'windows' - - 'C:\\Program Files\\Puppet Labs\\Puppet\\puppet\\bin' -end - -def puppet_bin_dir - return '/opt/puppetlabs/puppet/bin' unless HOST_PLATFORM.include? 'windows' - - 'C:\\Program Files\\Puppet Labs\\Puppet\\bin' -end - -def puppet_ruby - return '/opt/puppetlabs/puppet/bin/ruby' unless HOST_PLATFORM.include? 'windows' - - 'C:\\Program Files\\Puppet Labs\\Puppet\\puppet\\bin\\ruby.exe' -end - -def facter_lib_path - return '/opt/puppetlabs/puppet/lib/ruby/vendor_ruby/facter' unless HOST_PLATFORM.include? 'windows' - - 'C:\\Program Files\\Puppet Labs\\Puppet\\puppet\\lib\\ruby\\vendor_ruby\\facter' -end - -def env_path_var - HOST_PLATFORM.include?('windows') ? { 'PATH' => "#{puppet_bin_dir};#{ENV['PATH']}" } : {} -end - -def install_facter - message('OVERWRITE FACTER FROM PUPPET AGENT') - - # clean facter directory - FileUtils.rm_r(facter_lib_path) - FileUtils.mkdir(facter_lib_path) - - Dir.chdir('../') do - run("\'#{puppet_ruby}\' install.rb --bindir=\'#{puppet_puppet_bin_dir}\' " \ - "--sitelibdir=\'#{facter_lib_path.gsub('facter', '')}\'") - end -end - -def run_acceptance_tests - message('RUN ACCEPTANCE TESTS') - - run('beaker exec tests --test-tag-exclude=server,facter_3 --test-tag-or=risk:high,audit:high', './', env_path_var) -end - -def message(message) - message_length = message.length - total_length = 130 - lines_length = (total_length - message_length) / 2 - result = ('-' * lines_length + ' ' + message + ' ' + '-' * lines_length)[0, total_length] - puts "\n\n#{result}\n\n" -end - -def run(command, dir = './', env = {}) - puts command - output = '' - Open3.popen2e(env, command, chdir: dir) do |_stdin, stdout_and_err, wait_thr| - stdout_and_err.each do |line| - puts line - output += line - end - exit_status = wait_thr.value.exitstatus - exit(exit_status) if exit_status != 0 - end - output -end - -def verify_facter_standalone_exits_0 - Dir.chdir(ENV['FACTER_ROOT']) do - run('bundle install --without documentation') - run('bundle exec facter') - end -end - -ENV['DEBIAN_DISABLE_RUBYGEMS_INTEGRATION'] = 'no_warnings' -if_no_env_vars_set_defaults -ACCEPTANCE_PATH = File.join(ENV['FACTER_ROOT'], 'acceptance') -HOST_PLATFORM = ARGV[0] - -install_bundler - -verify_facter_standalone_exits_0 - -Dir.chdir(ACCEPTANCE_PATH) do - install_facter_acceptance_dependencies - initialize_beaker - install_puppet_agent - install_facter - run_acceptance_tests -end diff --git a/.github/workflows/acceptance_tests.yml b/.github/workflows/acceptance_tests.yml deleted file mode 100644 index cc321f8f28..0000000000 --- a/.github/workflows/acceptance_tests.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: Acceptance tests - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - acceptance_tests: - name: Platform - strategy: - matrix: - os: [ windows-2019, ubuntu-20.04, macos-12 ] - runs-on: ${{ matrix.os }} - env: - BEAKER_debug: true - FACTER_ROOT: facter - SHA: latest - RELEASE_STREAM: puppet7 - - steps: - - name: Checkout current PR - uses: actions/checkout@v4 - with: - path: facter - - - name: Install Ruby 2.7 - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - rubygems: '3.3.26' - - - name: Fix common Linux and macOS permissions - if: runner.os != 'Windows' - run: sudo chmod a-w /opt - - - name: Fix Linux permissions - if: runner.os == 'Linux' - run: | - sudo chmod a-w /home/runner /usr/share && - sudo chmod -R a-w /home/runner/.config /home/linuxbrew - - - name: Install dhclient for Linux - if: runner.os == 'Linux' - run: | - sudo apt install dhcpcd5 - sudo dhclient - - # IPv6 is missing on the GitHub macOS image and we need it for the networking facts tests - # https://github.com/actions/runner-images/issues/668 - - name: Add IPv6 on macOS - if: runner.os == 'macOS' - run: | - primary_interface=`route -n get default | awk '/interface: */{print $NF}'` - sudo ifconfig $primary_interface inet6 add ::1/64 - - - name: Run acceptance tests on Linux-like platform - if: runner.os != 'Windows' - run: sudo -E "PATH=$PATH" ruby $FACTER_ROOT/.github/actions/presuite.rb ${{ matrix.os }} - - - name: Run acceptance tests on Windows-like platform - if: runner.os == 'Windows' - run: ruby $Env:FACTER_ROOT/.github/actions/presuite.rb ${{ matrix.os }} From ac9c24b860725722250f63055c950c0cfc47e1f3 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 24 Apr 2025 15:34:00 -0700 Subject: [PATCH 28/29] Use ubuntu-latest Ubuntu 20.04 was retired 2025-04-15 This follows the same thing we did in puppetlabs/phoenix-github-actions@36a826a86670fc05f9b9539052501999b9b12f96 --- .github/workflows/checks.yaml | 2 +- .github/workflows/unit_tests.yaml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 8cc88f0f4c..a4e14c9c7f 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -11,7 +11,7 @@ on: jobs: rubocop_checks: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest name: RuboCop steps: - name: Checkout current PR diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml index cd0ee2bc5f..e5ffc3d0fc 100644 --- a/.github/workflows/unit_tests.yaml +++ b/.github/workflows/unit_tests.yaml @@ -24,7 +24,9 @@ jobs: - '3.2' - 'jruby-9.3.14.0' - 'jruby-9.4.8.0' - runs-on: ubuntu-20.04 + # 24.04 doesn't support older jruby versions, so use 22.04 as middle ground + # When we drop testing against puppet7, then we can use newer ubuntu. + runs-on: ubuntu-22.04 steps: - name: Checkout current PR uses: actions/checkout@v4 From fc561c9cf7dc497b1c51a6933c1f4471bc16806d Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 1 Jul 2025 10:03:51 -0700 Subject: [PATCH 29/29] (maint) Bump to windows-2022 https://github.com/actions/runner-images/issues/12045 --- .github/workflows/integration_tests.yaml | 4 ++-- .github/workflows/unit_tests.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index 29bcfecc7b..c0f9a8e80a 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -22,8 +22,8 @@ jobs: - {os: ubuntu-22.04, ruby: '3.2'} # with openssl 3 - {os: ubuntu-22.04, ruby: 'jruby-9.3.14.0'} - {os: ubuntu-latest, ruby: 'jruby-9.4.8.0'} - - {os: windows-2019, ruby: '2.7'} - - {os: windows-2019, ruby: '3.2'} # with openssl 3 + - {os: windows-2022, ruby: '2.7'} + - {os: windows-2022, ruby: '3.2'} # with openssl 3 runs-on: ${{ matrix.cfg.os }} env: BUNDLE_SET: 'with integration' diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml index e5ffc3d0fc..e05104ca0c 100644 --- a/.github/workflows/unit_tests.yaml +++ b/.github/workflows/unit_tests.yaml @@ -45,7 +45,7 @@ jobs: ruby: - '2.7' - '3.2' - runs-on: windows-2019 + runs-on: windows-2022 steps: - name: Checkout current PR uses: actions/checkout@v4