|
| 1 | +# Public: A puppet-lint custom check to detect legacy facts. |
| 2 | +# |
| 3 | +# This check will optionally convert from legacy facts like $::operatingsystem |
| 4 | +# or legacy hashed facts like $facts['operatingsystem'] to the |
| 5 | +# new structured facts like $facts['os']['name']. |
| 6 | +# |
| 7 | +# This plugin was adopted in to puppet-lint from https://github.com/mmckinst/puppet-lint-legacy_facts-check |
| 8 | +# Thanks to @mmckinst, @seanmil, @rodjek, @baurmatt, @bart2 and @joshcooper for the original work. |
| 9 | +PuppetLint.new_check(:legacy_facts) do |
| 10 | + LEGACY_FACTS_VAR_TYPES = Set[:VARIABLE, :UNENC_VARIABLE] |
| 11 | + |
| 12 | + # These facts that can't be converted to new facts. |
| 13 | + UNCONVERTIBLE_FACTS = ['memoryfree_mb', 'memorysize_mb', 'swapfree_mb', |
| 14 | + 'swapsize_mb', 'blockdevices', 'interfaces', 'zones', |
| 15 | + 'sshfp_dsa', 'sshfp_ecdsa', 'sshfp_ed25519', |
| 16 | + 'sshfp_rsa'].freeze |
| 17 | + |
| 18 | + # These facts will depend on how a system is set up and can't just be |
| 19 | + # enumerated like the EASY_FACTS below. |
| 20 | + # |
| 21 | + # For example a server might have two block devices named 'sda' and 'sdb' so |
| 22 | + # there would be a $blockdeivce_sda_vendor and $blockdeivce_sdb_vendor fact |
| 23 | + # for each device. Or it could have 26 block devices going all the way up to |
| 24 | + # 'sdz'. There is no way to know what the possibilities are so we have to use |
| 25 | + # a regex to match them. |
| 26 | + REGEX_FACTS = [%r{^blockdevice_(?<devicename>.*)_(?<attribute>model|size|vendor)$}, |
| 27 | + %r{^(?<attribute>ipaddress|ipaddress6|macaddress|mtu|netmask|netmask6|network|network6)_(?<interface>.*)$}, |
| 28 | + %r{^processor(?<id>[0-9]+)$}, |
| 29 | + %r{^sp_(?<name>.*)$}, |
| 30 | + %r{^ssh(?<algorithm>dsa|ecdsa|ed25519|rsa)key$}, |
| 31 | + %r{^ldom_(?<name>.*)$}, |
| 32 | + %r{^zone_(?<name>.*)_(?<attribute>brand|iptype|name|uuid|id|path|status)$}].freeze |
| 33 | + |
| 34 | + # These facts have a one to one correlation between a legacy fact and a new |
| 35 | + # structured fact. |
| 36 | + EASY_FACTS = { |
| 37 | + 'architecture' => "facts['os']['architecture']", |
| 38 | + 'augeasversion' => "facts['augeas']['version']", |
| 39 | + 'bios_release_date' => "facts['dmi']['bios']['release_date']", |
| 40 | + 'bios_vendor' => "facts['dmi']['bios']['vendor']", |
| 41 | + 'bios_version' => "facts['dmi']['bios']['version']", |
| 42 | + 'boardassettag' => "facts['dmi']['board']['asset_tag']", |
| 43 | + 'boardmanufacturer' => "facts['dmi']['board']['manufacturer']", |
| 44 | + 'boardproductname' => "facts['dmi']['board']['product']", |
| 45 | + 'boardserialnumber' => "facts['dmi']['board']['serial_number']", |
| 46 | + 'chassisassettag' => "facts['dmi']['chassis']['asset_tag']", |
| 47 | + 'chassistype' => "facts['dmi']['chassis']['type']", |
| 48 | + 'domain' => "facts['networking']['domain']", |
| 49 | + 'fqdn' => "facts['networking']['fqdn']", |
| 50 | + 'gid' => "facts['identity']['group']", |
| 51 | + 'hardwareisa' => "facts['processors']['isa']", |
| 52 | + 'hardwaremodel' => "facts['os']['hardware']", |
| 53 | + 'hostname' => "facts['networking']['hostname']", |
| 54 | + 'id' => "facts['identity']['user']", |
| 55 | + 'ipaddress' => "facts['networking']['ip']", |
| 56 | + 'ipaddress6' => "facts['networking']['ip6']", |
| 57 | + 'lsbdistcodename' => "facts['os']['distro']['codename']", |
| 58 | + 'lsbdistdescription' => "facts['os']['distro']['description']", |
| 59 | + 'lsbdistid' => "facts['os']['distro']['id']", |
| 60 | + 'lsbdistrelease' => "facts['os']['distro']['release']['full']", |
| 61 | + 'lsbmajdistrelease' => "facts['os']['distro']['release']['major']", |
| 62 | + 'lsbminordistrelease' => "facts['os']['distro']['release']['minor']", |
| 63 | + 'lsbrelease' => "facts['os']['distro']['release']['specification']", |
| 64 | + 'macaddress' => "facts['networking']['mac']", |
| 65 | + 'macosx_buildversion' => "facts['os']['build']", |
| 66 | + 'macosx_productname' => "facts['os']['product']", |
| 67 | + 'macosx_productversion' => "facts['os']['version']['full']", |
| 68 | + 'macosx_productversion_major' => "facts['os']['version']['major']", |
| 69 | + 'macosx_productversion_minor' => "facts['os']['version']['minor']", |
| 70 | + 'manufacturer' => "facts['dmi']['manufacturer']", |
| 71 | + 'memoryfree' => "facts['memory']['system']['available']", |
| 72 | + 'memorysize' => "facts['memory']['system']['total']", |
| 73 | + 'netmask' => "facts['networking']['netmask']", |
| 74 | + 'netmask6' => "facts['networking']['netmask6']", |
| 75 | + 'network' => "facts['networking']['network']", |
| 76 | + 'network6' => "facts['networking']['network6']", |
| 77 | + 'operatingsystem' => "facts['os']['name']", |
| 78 | + 'operatingsystemmajrelease' => "facts['os']['release']['major']", |
| 79 | + 'operatingsystemrelease' => "facts['os']['release']['full']", |
| 80 | + 'osfamily' => "facts['os']['family']", |
| 81 | + 'physicalprocessorcount' => "facts['processors']['physicalcount']", |
| 82 | + 'processorcount' => "facts['processors']['count']", |
| 83 | + 'productname' => "facts['dmi']['product']['name']", |
| 84 | + 'rubyplatform' => "facts['ruby']['platform']", |
| 85 | + 'rubysitedir' => "facts['ruby']['sitedir']", |
| 86 | + 'rubyversion' => "facts['ruby']['version']", |
| 87 | + 'selinux' => "facts['os']['selinux']['enabled']", |
| 88 | + 'selinux_config_mode' => "facts['os']['selinux']['config_mode']", |
| 89 | + 'selinux_config_policy' => "facts['os']['selinux']['config_policy']", |
| 90 | + 'selinux_current_mode' => "facts['os']['selinux']['current_mode']", |
| 91 | + 'selinux_enforced' => "facts['os']['selinux']['enforced']", |
| 92 | + 'selinux_policyversion' => "facts['os']['selinux']['policy_version']", |
| 93 | + 'serialnumber' => "facts['dmi']['product']['serial_number']", |
| 94 | + 'swapencrypted' => "facts['memory']['swap']['encrypted']", |
| 95 | + 'swapfree' => "facts['memory']['swap']['available']", |
| 96 | + 'swapsize' => "facts['memory']['swap']['total']", |
| 97 | + 'system32' => "facts['os']['windows']['system32']", |
| 98 | + 'uptime' => "facts['system_uptime']['uptime']", |
| 99 | + 'uptime_days' => "facts['system_uptime']['days']", |
| 100 | + 'uptime_hours' => "facts['system_uptime']['hours']", |
| 101 | + 'uptime_seconds' => "facts['system_uptime']['seconds']", |
| 102 | + 'uuid' => "facts['dmi']['product']['uuid']", |
| 103 | + 'xendomains' => "facts['xen']['domains']", |
| 104 | + 'zonename' => "facts['solaris_zones']['current']", |
| 105 | + }.freeze |
| 106 | + |
| 107 | + # A list of valid hash key token types |
| 108 | + HASH_KEY_TYPES = Set[ |
| 109 | + :STRING, # Double quoted string |
| 110 | + :SSTRING, # Single quoted string |
| 111 | + :NAME, # Unquoted single word |
| 112 | + ].freeze |
| 113 | + |
| 114 | + def check |
| 115 | + tokens.select { |x| LEGACY_FACTS_VAR_TYPES.include?(x.type) }.each do |token| |
| 116 | + fact_name = '' |
| 117 | + |
| 118 | + # Get rid of the top scope before we do our work. We don't need to |
| 119 | + # preserve it because it won't work with the new structured facts. |
| 120 | + if token.value.start_with?('::') |
| 121 | + fact_name = token.value.sub(%r{^::}, '') |
| 122 | + |
| 123 | + # This matches using legacy facts in a the new structured fact. For |
| 124 | + # example this would match 'uuid' in $facts['uuid'] so it can be converted |
| 125 | + # to facts['dmi']['product']['uuid']" |
| 126 | + elsif token.value == 'facts' |
| 127 | + fact_name = hash_key_for(token) |
| 128 | + |
| 129 | + elsif token.value.start_with?("facts['") |
| 130 | + fact_name = token.value.match(%r{facts\['(.*)'\]})[1] |
| 131 | + end |
| 132 | + |
| 133 | + next unless EASY_FACTS.include?(fact_name) || UNCONVERTIBLE_FACTS.include?(fact_name) || fact_name.match(Regexp.union(REGEX_FACTS)) |
| 134 | + notify :warning, { |
| 135 | + message: "legacy fact '#{fact_name}'", |
| 136 | + line: token.line, |
| 137 | + column: token.column, |
| 138 | + token: token, |
| 139 | + fact_name: fact_name, |
| 140 | + } |
| 141 | + end |
| 142 | + end |
| 143 | + |
| 144 | + # If the variable is using the $facts hash represented internally by multiple |
| 145 | + # tokens, this helper simplifies accessing the hash key. |
| 146 | + def hash_key_for(token) |
| 147 | + lbrack_token = token.next_code_token |
| 148 | + return '' unless lbrack_token && lbrack_token.type == :LBRACK |
| 149 | + |
| 150 | + key_token = lbrack_token.next_code_token |
| 151 | + return '' unless key_token && HASH_KEY_TYPES.include?(key_token.type) |
| 152 | + |
| 153 | + key_token.value |
| 154 | + end |
| 155 | + |
| 156 | + def fix(problem) |
| 157 | + fact_name = problem[:fact_name] |
| 158 | + |
| 159 | + # Check if the variable is using the $facts hash represented internally by |
| 160 | + # multiple tokens and remove the tokens for the old legacy key if so. |
| 161 | + if problem[:token].value == 'facts' |
| 162 | + loop do |
| 163 | + t = problem[:token].next_token |
| 164 | + remove_token(t) |
| 165 | + break if t.type == :RBRACK |
| 166 | + end |
| 167 | + end |
| 168 | + |
| 169 | + if EASY_FACTS.include?(fact_name) |
| 170 | + problem[:token].value = EASY_FACTS[fact_name] |
| 171 | + elsif fact_name.match(Regexp.union(REGEX_FACTS)) |
| 172 | + if (m = fact_name.match(%r{^blockdevice_(?<devicename>.*)_(?<attribute>model|size|vendor)$})) |
| 173 | + problem[:token].value = "facts['disks']['" << m['devicename'] << "']['" << m['attribute'] << "']" |
| 174 | + elsif (m = fact_name.match(%r{^(?<attribute>ipaddress|ipaddress6|macaddress|mtu|netmask|netmask6|network|network6)_(?<interface>.*)$})) |
| 175 | + problem[:token].value = "facts['networking']['interfaces']['" << m['interface'] << "']['" << m['attribute'].sub('address', '') << "']" |
| 176 | + elsif (m = fact_name.match(%r{^processor(?<id>[0-9]+)$})) |
| 177 | + problem[:token].value = "facts['processors']['models'][" << m['id'] << ']' |
| 178 | + elsif (m = fact_name.match(%r{^sp_(?<name>.*)$})) |
| 179 | + problem[:token].value = "facts['system_profiler']['" << m['name'] << "']" |
| 180 | + elsif (m = fact_name.match(%r{^ssh(?<algorithm>dsa|ecdsa|ed25519|rsa)key$})) |
| 181 | + problem[:token].value = "facts['ssh']['" << m['algorithm'] << "']['key']" |
| 182 | + elsif (m = fact_name.match(%r{^ldom_(?<name>.*)$})) |
| 183 | + problem[:token].value = "facts['ldom']['" << m['name'] << "']" |
| 184 | + elsif (m = fact_name.match(%r{^zone_(?<name>.*)_(?<attribute>brand|iptype|name|uuid|id|path|status)$})) |
| 185 | + problem[:token].value = "facts['solaris_zones']['zones']['" << m['name'] << "']['" << m['attribute'] << "']" |
| 186 | + end |
| 187 | + end |
| 188 | + end |
| 189 | +end |
0 commit comments