lib/ohai/mixin/ec2_metadata.rb in ohai-18.1.18 vs lib/ohai/mixin/ec2_metadata.rb in ohai-19.0.3

- old
+ new

@@ -1,264 +1,264 @@ -# frozen_string_literal: true -# -# Author:: Tim Dysinger (<tim@dysinger.net>) -# Author:: Benjamin Black (<bb@chef.io>) -# Author:: Christopher Brown (<cb@chef.io>) -# Copyright:: Copyright (c) Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require "net/http" unless defined?(Net::HTTP) - -require_relative "../mixin/json_helper" -include Ohai::Mixin::JsonHelper - -module Ohai - module Mixin - ## - # This code parses the EC2 Instance Metadata API to provide details - # of the running instance. - # - # Earlier version of this code assumed a specific version of the - # metadata API was available. Unfortunately the API versions - # supported by a particular instance are determined at instance - # launch and are not extended over the life of the instance. As such - # the earlier code would fail depending on the age of the instance. - # - # The updated code probes the instance metadata endpoint for - # available versions, determines the most advanced version known to - # work and executes the metadata retrieval using that version. - # - # If no compatible version is found, an empty hash is returned. - # - module Ec2Metadata - - EC2_METADATA_ADDR ||= "169.254.169.254" - EC2_SUPPORTED_VERSIONS ||= %w{ 1.0 - 2007-01-19 - 2007-03-01 - 2007-08-29 - 2007-10-10 - 2007-12-15 - 2008-02-01 - 2008-09-01 - 2009-04-04 - 2011-01-01 - 2011-05-01 - 2012-01-12 - 2014-02-25 - 2014-11-05 - 2015-10-20 - 2016-04-19 - 2016-06-30 - 2016-09-02 - 2018-03-28 - 2018-08-17 - 2018-09-24 - 2019-10-01 - 2020-10-27 - 2021-01-03 - 2021-03-23 - 2021-07-15 }.freeze - EC2_ARRAY_VALUES ||= %w{security-groups local_ipv4s}.freeze - EC2_ARRAY_DIR ||= %w{network/interfaces/macs}.freeze - EC2_JSON_DIR ||= %w{iam}.freeze - - # - # The latest metadata version in EC2_SUPPORTED_VERSIONS that this instance supports - # in AWS supported metadata versions are determined at instance start so we need to be - # cautious here in case an instance has been running for a long time - # - # @return [String] the version - # - def best_api_version - @api_version ||= begin - logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}/ to determine the latest supported metadata release") - response = http_client.get("/", { 'X-aws-ec2-metadata-token': v2_token }) - if response.code == "404" - logger.trace("Mixin EC2: Received HTTP 404 from metadata server while determining API version, assuming 'latest'") - return "latest" - elsif response.code != "200" - raise "Mixin EC2: Unable to determine EC2 metadata version (returned #{response.code} response)" - end - # NOTE: Sorting the list of versions may have unintended consequences in - # non-EC2 environments. It appears to be safe in EC2 as of 2013-04-12. - versions = response.body.split("\n").sort - until versions.empty? || EC2_SUPPORTED_VERSIONS.include?(versions.last) - pv = versions.pop - logger.trace("Mixin EC2: EC2 lists metadata version: #{pv} not yet supported by Ohai") unless pv == "latest" - end - logger.trace("Mixin EC2: Latest supported EC2 metadata version: #{versions.last}") - if versions.empty? - raise "Mixin EC2: Unable to determine EC2 metadata version (no supported entries found)" - end - - versions.last - end - end - - # a net/http client with a timeout of 10s and a keepalive of 10s - # - # @return [Net::HTTP] - def http_client - @conn ||= Net::HTTP.start(EC2_METADATA_ADDR).tap do |h| - h.read_timeout = 10 - h.keep_alive_timeout = 10 - end - end - - # - # Fetch an API token for use querying AWS IMDSv2 or return nil if no token if found - # AWS like systems (think OpenStack) will not respond with a token here - # - # @return [NilClass, String] API token or nil - # - def v2_token - @v2_token ||= begin - request = http_client.put("/latest/api/token", nil, { 'X-aws-ec2-metadata-token-ttl-seconds': "60" }) - if request.code == "404" # not on AWS - nil - else - request.body - end - end - end - - # Get metadata for a given path and API version - # - # Typically, a 200 response is expected for valid metadata. - # On certain instance types, traversing the provided metadata path - # produces a 404 for some unknown reason. In that event, return - # `nil` and continue the run instead of failing it. - def metadata_get(id, api_version) - path = "/#{api_version}/meta-data/#{id}" - logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}#{path}") - response = http_client.get(path, { 'X-aws-ec2-metadata-token': v2_token }) - case response.code - when "200" - response.body - when "404" - logger.trace("Mixin EC2: Encountered 404 response retrieving EC2 metadata path: #{path} ; continuing.") - nil - else - raise "Mixin EC2: Encountered error retrieving EC2 metadata (#{path} returned #{response.code} response)" - end - end - - def fetch_metadata(id = "", api_version = nil) - metadata = {} - retrieved_metadata = metadata_get(id, best_api_version) - if retrieved_metadata - retrieved_metadata.split("\n").each do |o| - key = expand_path("#{id}#{o}") - if key[-1..-1] != "/" - metadata[metadata_key(key)] = - if EC2_ARRAY_VALUES.include? key - retr_meta = metadata_get(key, best_api_version) - retr_meta ? retr_meta.split("\n") : retr_meta - else - metadata_get(key, best_api_version) - end - elsif (!key.eql?(id)) && (!key.eql?("/")) - name = key[0..-2] - sym = metadata_key(name) - if EC2_ARRAY_DIR.include?(name) - metadata[sym] = fetch_dir_metadata(key, best_api_version) - elsif EC2_JSON_DIR.include?(name) - metadata[sym] = fetch_json_dir_metadata(key, best_api_version) - else - fetch_metadata(key, best_api_version).each { |k, v| metadata[k] = v } - end - end - end - metadata - end - end - - def fetch_dir_metadata(id, api_version) - metadata = {} - retrieved_metadata = metadata_get(id, api_version) - if retrieved_metadata - retrieved_metadata.split("\n").each do |o| - key = expand_path(o) - if key[-1..-1] != "/" - retr_meta = metadata_get("#{id}#{key}", api_version) - metadata[metadata_key(key)] = retr_meta || "" - elsif !key.eql?("/") - metadata[key[0..-2]] = fetch_dir_metadata("#{id}#{key}", api_version) - end - end - metadata - end - end - - def fetch_json_dir_metadata(id, api_version) - metadata = {} - retrieved_metadata = metadata_get(id, api_version) - if retrieved_metadata - retrieved_metadata.split("\n").each do |o| - key = expand_path(o) - if key[-1..-1] != "/" - retr_meta = metadata_get("#{id}#{key}", api_version) - data = retr_meta || "" - json = String(data) - parser = FFI_Yajl::Parser.new - metadata[metadata_key(key)] = parser.parse(json) - elsif !key.eql?("/") - metadata[key[0..-2]] = fetch_json_dir_metadata("#{id}#{key}", api_version) - end - end - metadata - end - end - - def fetch_userdata - logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}/#{best_api_version}/user-data/") - response = http_client.get("/#{best_api_version}/user-data/", { 'X-aws-ec2-metadata-token': v2_token }) - response.code == "200" ? response.body : nil - end - - def fetch_dynamic_data - @fetch_dynamic_data ||= begin - response = http_client.get("/#{best_api_version}/dynamic/instance-identity/document/", { 'X-aws-ec2-metadata-token': v2_token }) - - if response.code == "200" - json_data = parse_json(response.body, {}) - if json_data.nil? - logger.warn("Mixin Ec2Metadata: Metadata response is NOT valid JSON") - end - json_data - else - logger.warn("Mixin Ec2Metadata: Received response code #{response.code} requesting metadata") - {} - end - end - end - - private - - def expand_path(file_name) - path = file_name.gsub(/\=.*$/, "/") - # ignore "./" and "../" - path.gsub(%r{/\.\.?(?:/|$)}, "/") - .sub(%r{^\.\.?(?:/|$)}, "") - .sub(/^$/, "/") - end - - def metadata_key(key) - key.gsub(%r{\-|/}, "_") - end - - end - end -end +# frozen_string_literal: true +# +# Author:: Tim Dysinger (<tim@dysinger.net>) +# Author:: Benjamin Black (<bb@chef.io>) +# Author:: Christopher Brown (<cb@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "net/http" unless defined?(Net::HTTP) + +require_relative "../mixin/json_helper" +include Ohai::Mixin::JsonHelper + +module Ohai + module Mixin + ## + # This code parses the EC2 Instance Metadata API to provide details + # of the running instance. + # + # Earlier version of this code assumed a specific version of the + # metadata API was available. Unfortunately the API versions + # supported by a particular instance are determined at instance + # launch and are not extended over the life of the instance. As such + # the earlier code would fail depending on the age of the instance. + # + # The updated code probes the instance metadata endpoint for + # available versions, determines the most advanced version known to + # work and executes the metadata retrieval using that version. + # + # If no compatible version is found, an empty hash is returned. + # + module Ec2Metadata + + EC2_METADATA_ADDR ||= "169.254.169.254" + EC2_SUPPORTED_VERSIONS ||= %w{ 1.0 + 2007-01-19 + 2007-03-01 + 2007-08-29 + 2007-10-10 + 2007-12-15 + 2008-02-01 + 2008-09-01 + 2009-04-04 + 2011-01-01 + 2011-05-01 + 2012-01-12 + 2014-02-25 + 2014-11-05 + 2015-10-20 + 2016-04-19 + 2016-06-30 + 2016-09-02 + 2018-03-28 + 2018-08-17 + 2018-09-24 + 2019-10-01 + 2020-10-27 + 2021-01-03 + 2021-03-23 + 2021-07-15 }.freeze + EC2_ARRAY_VALUES ||= %w{security-groups local_ipv4s}.freeze + EC2_ARRAY_DIR ||= %w{network/interfaces/macs}.freeze + EC2_JSON_DIR ||= %w{iam}.freeze + + # + # The latest metadata version in EC2_SUPPORTED_VERSIONS that this instance supports + # in AWS supported metadata versions are determined at instance start so we need to be + # cautious here in case an instance has been running for a long time + # + # @return [String] the version + # + def best_api_version + @api_version ||= begin + logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}/ to determine the latest supported metadata release") + response = http_client.get("/", { 'X-aws-ec2-metadata-token': v2_token }) + if response.code == "404" + logger.trace("Mixin EC2: Received HTTP 404 from metadata server while determining API version, assuming 'latest'") + return "latest" + elsif response.code != "200" + raise "Mixin EC2: Unable to determine EC2 metadata version (returned #{response.code} response)" + end + # NOTE: Sorting the list of versions may have unintended consequences in + # non-EC2 environments. It appears to be safe in EC2 as of 2013-04-12. + versions = response.body.split("\n").sort + until versions.empty? || EC2_SUPPORTED_VERSIONS.include?(versions.last) + pv = versions.pop + logger.trace("Mixin EC2: EC2 lists metadata version: #{pv} not yet supported by Ohai") unless pv == "latest" + end + logger.trace("Mixin EC2: Latest supported EC2 metadata version: #{versions.last}") + if versions.empty? + raise "Mixin EC2: Unable to determine EC2 metadata version (no supported entries found)" + end + + versions.last + end + end + + # a net/http client with a timeout of 10s and a keepalive of 10s + # + # @return [Net::HTTP] + def http_client + @conn ||= Net::HTTP.start(EC2_METADATA_ADDR).tap do |h| + h.read_timeout = 10 + h.keep_alive_timeout = 10 + end + end + + # + # Fetch an API token for use querying AWS IMDSv2 or return nil if no token if found + # AWS like systems (think OpenStack) will not respond with a token here + # + # @return [NilClass, String] API token or nil + # + def v2_token + @v2_token ||= begin + request = http_client.put("/latest/api/token", nil, { 'X-aws-ec2-metadata-token-ttl-seconds': "60" }) + if request.code == "404" # not on AWS + nil + else + request.body + end + end + end + + # Get metadata for a given path and API version + # + # Typically, a 200 response is expected for valid metadata. + # On certain instance types, traversing the provided metadata path + # produces a 404 for some unknown reason. In that event, return + # `nil` and continue the run instead of failing it. + def metadata_get(id, api_version) + path = "/#{api_version}/meta-data/#{id}" + logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}#{path}") + response = http_client.get(path, { 'X-aws-ec2-metadata-token': v2_token }) + case response.code + when "200" + response.body + when "404" + logger.trace("Mixin EC2: Encountered 404 response retrieving EC2 metadata path: #{path} ; continuing.") + nil + else + raise "Mixin EC2: Encountered error retrieving EC2 metadata (#{path} returned #{response.code} response)" + end + end + + def fetch_metadata(id = "", api_version = nil) + metadata = {} + retrieved_metadata = metadata_get(id, best_api_version) + if retrieved_metadata + retrieved_metadata.split("\n").each do |o| + key = expand_path("#{id}#{o}") + if key[-1..-1] != "/" + metadata[metadata_key(key)] = + if EC2_ARRAY_VALUES.include? key + retr_meta = metadata_get(key, best_api_version) + retr_meta ? retr_meta.split("\n") : retr_meta + else + metadata_get(key, best_api_version) + end + elsif (!key.eql?(id)) && (!key.eql?("/")) + name = key[0..-2] + sym = metadata_key(name) + if EC2_ARRAY_DIR.include?(name) + metadata[sym] = fetch_dir_metadata(key, best_api_version) + elsif EC2_JSON_DIR.include?(name) + metadata[sym] = fetch_json_dir_metadata(key, best_api_version) + else + fetch_metadata(key, best_api_version).each { |k, v| metadata[k] = v } + end + end + end + metadata + end + end + + def fetch_dir_metadata(id, api_version) + metadata = {} + retrieved_metadata = metadata_get(id, api_version) + if retrieved_metadata + retrieved_metadata.split("\n").each do |o| + key = expand_path(o) + if key[-1..-1] != "/" + retr_meta = metadata_get("#{id}#{key}", api_version) + metadata[metadata_key(key)] = retr_meta || "" + elsif !key.eql?("/") + metadata[key[0..-2]] = fetch_dir_metadata("#{id}#{key}", api_version) + end + end + metadata + end + end + + def fetch_json_dir_metadata(id, api_version) + metadata = {} + retrieved_metadata = metadata_get(id, api_version) + if retrieved_metadata + retrieved_metadata.split("\n").each do |o| + key = expand_path(o) + if key[-1..-1] != "/" + retr_meta = metadata_get("#{id}#{key}", api_version) + data = retr_meta || "" + json = String(data) + parser = FFI_Yajl::Parser.new + metadata[metadata_key(key)] = parser.parse(json) + elsif !key.eql?("/") + metadata[key[0..-2]] = fetch_json_dir_metadata("#{id}#{key}", api_version) + end + end + metadata + end + end + + def fetch_userdata + logger.trace("Mixin EC2: Fetching http://#{EC2_METADATA_ADDR}/#{best_api_version}/user-data/") + response = http_client.get("/#{best_api_version}/user-data/", { 'X-aws-ec2-metadata-token': v2_token }) + response.code == "200" ? response.body : nil + end + + def fetch_dynamic_data + @fetch_dynamic_data ||= begin + response = http_client.get("/#{best_api_version}/dynamic/instance-identity/document/", { 'X-aws-ec2-metadata-token': v2_token }) + + if response.code == "200" + json_data = parse_json(response.body, {}) + if json_data.nil? + logger.warn("Mixin Ec2Metadata: Metadata response is NOT valid JSON") + end + json_data + else + logger.warn("Mixin Ec2Metadata: Received response code #{response.code} requesting metadata") + {} + end + end + end + + private + + def expand_path(file_name) + path = file_name.gsub(/\=.*$/, "/") + # ignore "./" and "../" + path.gsub(%r{/\.\.?(?:/|$)}, "/") + .sub(%r{^\.\.?(?:/|$)}, "") + .sub(/^$/, "/") + end + + def metadata_key(key) + key.gsub(%r{\-|/}, "_") + end + + end + end +end