diff --git a/lib/puppet-lint/plugins/legacy_facts/legacy_facts.rb b/lib/puppet-lint/plugins/legacy_facts/legacy_facts.rb new file mode 100644 index 00000000..56c1949b --- /dev/null +++ b/lib/puppet-lint/plugins/legacy_facts/legacy_facts.rb @@ -0,0 +1,189 @@ +# Public: A puppet-lint custom check to detect legacy facts. +# +# This check will optionally convert from legacy facts like $::operatingsystem +# or legacy hashed facts like $facts['operatingsystem'] to the +# new structured facts like $facts['os']['name']. +# +# This plugin was adopted in to puppet-lint from https://github.com/mmckinst/puppet-lint-legacy_facts-check +# Thanks to @mmckinst, @seanmil, @rodjek, @baurmatt, @bart2 and @joshcooper for the original work. +PuppetLint.new_check(:legacy_facts) do + LEGACY_FACTS_VAR_TYPES = Set[:VARIABLE, :UNENC_VARIABLE] + + # These facts that can't be converted to new facts. + UNCONVERTIBLE_FACTS = ['memoryfree_mb', 'memorysize_mb', 'swapfree_mb', + 'swapsize_mb', 'blockdevices', 'interfaces', 'zones', + 'sshfp_dsa', 'sshfp_ecdsa', 'sshfp_ed25519', + 'sshfp_rsa'].freeze + + # These facts will depend on how a system is set up and can't just be + # enumerated like the EASY_FACTS below. + # + # For example a server might have two block devices named 'sda' and 'sdb' so + # there would be a $blockdeivce_sda_vendor and $blockdeivce_sdb_vendor fact + # for each device. Or it could have 26 block devices going all the way up to + # 'sdz'. There is no way to know what the possibilities are so we have to use + # a regex to match them. + REGEX_FACTS = [%r{^blockdevice_(?.*)_(?model|size|vendor)$}, + %r{^(?ipaddress|ipaddress6|macaddress|mtu|netmask|netmask6|network|network6)_(?.*)$}, + %r{^processor(?[0-9]+)$}, + %r{^sp_(?.*)$}, + %r{^ssh(?dsa|ecdsa|ed25519|rsa)key$}, + %r{^ldom_(?.*)$}, + %r{^zone_(?.*)_(?brand|iptype|name|uuid|id|path|status)$}].freeze + + # These facts have a one to one correlation between a legacy fact and a new + # structured fact. + EASY_FACTS = { + 'architecture' => "facts['os']['architecture']", + 'augeasversion' => "facts['augeas']['version']", + 'bios_release_date' => "facts['dmi']['bios']['release_date']", + 'bios_vendor' => "facts['dmi']['bios']['vendor']", + 'bios_version' => "facts['dmi']['bios']['version']", + 'boardassettag' => "facts['dmi']['board']['asset_tag']", + 'boardmanufacturer' => "facts['dmi']['board']['manufacturer']", + 'boardproductname' => "facts['dmi']['board']['product']", + 'boardserialnumber' => "facts['dmi']['board']['serial_number']", + 'chassisassettag' => "facts['dmi']['chassis']['asset_tag']", + 'chassistype' => "facts['dmi']['chassis']['type']", + 'domain' => "facts['networking']['domain']", + 'fqdn' => "facts['networking']['fqdn']", + 'gid' => "facts['identity']['group']", + 'hardwareisa' => "facts['processors']['isa']", + 'hardwaremodel' => "facts['os']['hardware']", + 'hostname' => "facts['networking']['hostname']", + 'id' => "facts['identity']['user']", + 'ipaddress' => "facts['networking']['ip']", + 'ipaddress6' => "facts['networking']['ip6']", + 'lsbdistcodename' => "facts['os']['distro']['codename']", + 'lsbdistdescription' => "facts['os']['distro']['description']", + 'lsbdistid' => "facts['os']['distro']['id']", + 'lsbdistrelease' => "facts['os']['distro']['release']['full']", + 'lsbmajdistrelease' => "facts['os']['distro']['release']['major']", + 'lsbminordistrelease' => "facts['os']['distro']['release']['minor']", + 'lsbrelease' => "facts['os']['distro']['release']['specification']", + 'macaddress' => "facts['networking']['mac']", + 'macosx_buildversion' => "facts['os']['build']", + 'macosx_productname' => "facts['os']['product']", + 'macosx_productversion' => "facts['os']['version']['full']", + 'macosx_productversion_major' => "facts['os']['version']['major']", + 'macosx_productversion_minor' => "facts['os']['version']['minor']", + 'manufacturer' => "facts['dmi']['manufacturer']", + 'memoryfree' => "facts['memory']['system']['available']", + 'memorysize' => "facts['memory']['system']['total']", + 'netmask' => "facts['networking']['netmask']", + 'netmask6' => "facts['networking']['netmask6']", + 'network' => "facts['networking']['network']", + 'network6' => "facts['networking']['network6']", + 'operatingsystem' => "facts['os']['name']", + 'operatingsystemmajrelease' => "facts['os']['release']['major']", + 'operatingsystemrelease' => "facts['os']['release']['full']", + 'osfamily' => "facts['os']['family']", + 'physicalprocessorcount' => "facts['processors']['physicalcount']", + 'processorcount' => "facts['processors']['count']", + 'productname' => "facts['dmi']['product']['name']", + 'rubyplatform' => "facts['ruby']['platform']", + 'rubysitedir' => "facts['ruby']['sitedir']", + 'rubyversion' => "facts['ruby']['version']", + 'selinux' => "facts['os']['selinux']['enabled']", + 'selinux_config_mode' => "facts['os']['selinux']['config_mode']", + 'selinux_config_policy' => "facts['os']['selinux']['config_policy']", + 'selinux_current_mode' => "facts['os']['selinux']['current_mode']", + 'selinux_enforced' => "facts['os']['selinux']['enforced']", + 'selinux_policyversion' => "facts['os']['selinux']['policy_version']", + 'serialnumber' => "facts['dmi']['product']['serial_number']", + 'swapencrypted' => "facts['memory']['swap']['encrypted']", + 'swapfree' => "facts['memory']['swap']['available']", + 'swapsize' => "facts['memory']['swap']['total']", + 'system32' => "facts['os']['windows']['system32']", + 'uptime' => "facts['system_uptime']['uptime']", + 'uptime_days' => "facts['system_uptime']['days']", + 'uptime_hours' => "facts['system_uptime']['hours']", + 'uptime_seconds' => "facts['system_uptime']['seconds']", + 'uuid' => "facts['dmi']['product']['uuid']", + 'xendomains' => "facts['xen']['domains']", + 'zonename' => "facts['solaris_zones']['current']", + }.freeze + + # A list of valid hash key token types + HASH_KEY_TYPES = Set[ + :STRING, # Double quoted string + :SSTRING, # Single quoted string + :NAME, # Unquoted single word + ].freeze + + def check + tokens.select { |x| LEGACY_FACTS_VAR_TYPES.include?(x.type) }.each do |token| + fact_name = '' + + # Get rid of the top scope before we do our work. We don't need to + # preserve it because it won't work with the new structured facts. + if token.value.start_with?('::') + fact_name = token.value.sub(%r{^::}, '') + + # This matches using legacy facts in a the new structured fact. For + # example this would match 'uuid' in $facts['uuid'] so it can be converted + # to facts['dmi']['product']['uuid']" + elsif token.value == 'facts' + fact_name = hash_key_for(token) + + elsif token.value.start_with?("facts['") + fact_name = token.value.match(%r{facts\['(.*)'\]})[1] + end + + next unless EASY_FACTS.include?(fact_name) || UNCONVERTIBLE_FACTS.include?(fact_name) || fact_name.match(Regexp.union(REGEX_FACTS)) + notify :warning, { + message: "legacy fact '#{fact_name}'", + line: token.line, + column: token.column, + token: token, + fact_name: fact_name, + } + end + end + + # If the variable is using the $facts hash represented internally by multiple + # tokens, this helper simplifies accessing the hash key. + def hash_key_for(token) + lbrack_token = token.next_code_token + return '' unless lbrack_token && lbrack_token.type == :LBRACK + + key_token = lbrack_token.next_code_token + return '' unless key_token && HASH_KEY_TYPES.include?(key_token.type) + + key_token.value + end + + def fix(problem) + fact_name = problem[:fact_name] + + # Check if the variable is using the $facts hash represented internally by + # multiple tokens and remove the tokens for the old legacy key if so. + if problem[:token].value == 'facts' + loop do + t = problem[:token].next_token + remove_token(t) + break if t.type == :RBRACK + end + end + + if EASY_FACTS.include?(fact_name) + problem[:token].value = EASY_FACTS[fact_name] + elsif fact_name.match(Regexp.union(REGEX_FACTS)) + if (m = fact_name.match(%r{^blockdevice_(?.*)_(?model|size|vendor)$})) + problem[:token].value = "facts['disks']['" << m['devicename'] << "']['" << m['attribute'] << "']" + elsif (m = fact_name.match(%r{^(?ipaddress|ipaddress6|macaddress|mtu|netmask|netmask6|network|network6)_(?.*)$})) + problem[:token].value = "facts['networking']['interfaces']['" << m['interface'] << "']['" << m['attribute'].sub('address', '') << "']" + elsif (m = fact_name.match(%r{^processor(?[0-9]+)$})) + problem[:token].value = "facts['processors']['models'][" << m['id'] << ']' + elsif (m = fact_name.match(%r{^sp_(?.*)$})) + problem[:token].value = "facts['system_profiler']['" << m['name'] << "']" + elsif (m = fact_name.match(%r{^ssh(?dsa|ecdsa|ed25519|rsa)key$})) + problem[:token].value = "facts['ssh']['" << m['algorithm'] << "']['key']" + elsif (m = fact_name.match(%r{^ldom_(?.*)$})) + problem[:token].value = "facts['ldom']['" << m['name'] << "']" + elsif (m = fact_name.match(%r{^zone_(?.*)_(?brand|iptype|name|uuid|id|path|status)$})) + problem[:token].value = "facts['solaris_zones']['zones']['" << m['name'] << "']['" << m['attribute'] << "']" + end + end + end +end diff --git a/spec/unit/puppet-lint/plugins/legacy_facts/legacy_facts_spec.rb b/spec/unit/puppet-lint/plugins/legacy_facts/legacy_facts_spec.rb new file mode 100644 index 00000000..0e43be14 --- /dev/null +++ b/spec/unit/puppet-lint/plugins/legacy_facts/legacy_facts_spec.rb @@ -0,0 +1,414 @@ +require 'spec_helper' + +describe 'legacy_facts' do + context 'with fix disabled' do + context "fact variable using modern $facts['os']['family'] hash" do + let(:code) { "$facts['os']['family']" } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context "fact variable using modern $facts['ssh']['rsa']['key'] hash" do + let(:code) { "$facts['ssh']['rsa']['key']" } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context 'fact variable using legacy $osfamily' do + let(:code) { '$osfamily' } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context "fact variable using legacy $facts['osfamily']" do + let(:code) { "$facts['osfamily']" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::osfamily' do + let(:code) { '$::osfamily' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::blockdevice_sda_model' do + let(:code) { '$::blockdevice_sda_model' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context "fact variable using legacy $facts['ipaddress6_em2']" do + let(:code) { "$facts['ipaddress6_em2']" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::zone_foobar_uuid' do + let(:code) { '$::zone_foobar_uuid' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::processor314' do + let(:code) { '$::processor314' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::sp_l3_cache' do + let(:code) { '$::sp_l3_cache' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy $::sshrsakey' do + let(:code) { '$::sshrsakey' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable in interpolated string "${::osfamily}"' do + let(:code) { '"start ${::osfamily} end"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy variable in double quotes "$::osfamily"' do + let(:code) { '"$::osfamily"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + end + + context 'fact variable using legacy facts hash variable in interpolation' do + let(:code) { %("${facts['osfamily']}") } + + it 'detects a single problem' do + expect(problems).to have(1).problem + end + end + end + + context 'with fix enabled' do + before(:each) do + PuppetLint.configuration.fix = true + end + + after(:each) do + PuppetLint.configuration.fix = false + end + + context "fact variable using modern $facts['os']['family'] hash" do + let(:code) { "$facts['os']['family']" } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context "fact variable using modern $facts['ssh']['rsa']['key'] hash" do + let(:code) { "$facts['ssh']['rsa']['key']" } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context 'fact variable using legacy $osfamily' do + let(:code) { '$osfamily' } + + it 'does not detect any problems' do + expect(problems).to have(0).problem + end + end + + context "fact variable using legacy $facts['osfamily']" do + let(:code) { "$facts['osfamily']" } + let(:msg) { "legacy fact 'osfamily'" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'fixes the problem' do + expect(problems).to contain_fixed(msg).on_line(1).in_column(1) + end + + it 'uses the facts hash' do + expect(manifest).to eq("$facts['os']['family']") + end + end + + context 'fact variable using legacy $::osfamily' do + let(:code) { '$::osfamily' } + let(:msg) { "legacy fact 'osfamily'" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'fixes the problem' do + expect(problems).to contain_fixed(msg).on_line(1).in_column(1) + end + + it 'uses the facts hash' do + expect(manifest).to eq("$facts['os']['family']") + end + end + + context 'fact variable using legacy $::sshrsakey' do + let(:code) { '$::sshrsakey' } + let(:msg) { "legacy fact 'sshrsakey'" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'fixes the problem' do + expect(problems).to contain_fixed(msg).on_line(1).in_column(1) + end + + it 'uses the facts hash' do + expect(manifest).to eq("$facts['ssh']['rsa']['key']") + end + end + + context 'fact variable using legacy $::memoryfree_mb' do + let(:code) { '$::memoryfree_mb' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'continues to use the legacy fact' do + expect(manifest).to eq('$::memoryfree_mb') + end + end + + context 'fact variable using legacy $::blockdevice_sda_model' do + let(:code) { '$::blockdevice_sda_model' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['disks']['sda']['model']") + end + end + + context "fact variable using legacy $facts['ipaddress6_em2']" do + let(:code) { "$facts['ipaddress6_em2']" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['networking']['interfaces']['em2']['ip6']") + end + end + + context 'fact variable using legacy $::zone_foobar_uuid' do + let(:code) { '$::zone_foobar_uuid' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['solaris_zones']['zones']['foobar']['uuid']") + end + end + + context 'fact variable using legacy $::processor314' do + let(:code) { '$::processor314' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['processors']['models'][314]") + end + end + + context 'fact variable using legacy $::sp_l3_cache' do + let(:code) { '$::sp_l3_cache' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['system_profiler']['l3_cache']") + end + end + + context 'fact variable using legacy $::sshrsakey' do + let(:code) { '$::sshrsakey' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + it 'uses the facts hash' do + expect(manifest).to eq("$facts['ssh']['rsa']['key']") + end + end + + context 'fact variable in interpolated string "${::osfamily}"' do + let(:code) { '"start ${::osfamily} end"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq('"start '"${facts['os']['family']}"' end"') # rubocop:disable Lint/ImplicitStringConcatenation + end + end + + context 'fact variable using legacy variable in double quotes "$::osfamily"' do + let(:code) { '"$::osfamily"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['family']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::gid"' do + let(:code) { '"$::gid"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['identity']['group']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::id"' do + let(:code) { '"$::id"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['identity']['user']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbdistcodename"' do + let(:code) { '"$::lsbdistcodename"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['codename']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbdistdescription"' do + let(:code) { '"$::lsbdistdescription"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['description']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbdistid"' do + let(:code) { '"$::lsbdistid"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['id']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbdistrelease"' do + let(:code) { '"$::lsbdistrelease"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['release']['full']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbmajdistrelease"' do + let(:code) { '"$::lsbmajdistrelease"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['release']['major']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbminordistrelease"' do + let(:code) { '"$::lsbminordistrelease"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['release']['minor']\"") + end + end + context 'fact variable using legacy variable in double quotes "$::lsbrelease"' do + let(:code) { '"$::lsbrelease"' } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"$facts['os']['distro']['release']['specification']\"") + end + end + context "fact variable using facts hash in double quotes \"$facts['lsbrelease']\"" do + let(:code) { "\"${facts['lsbrelease']}\"" } + + it 'onlies detect a single problem' do + expect(problems).to have(1).problem + end + + it 'uses the facts hash' do + expect(manifest).to eq("\"${facts['os']['distro']['release']['specification']}\"") + end + end + end +end