module Ohai::Mixin::Ec2Metadata

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.

Constants

EC2_ARRAY_DIR
EC2_ARRAY_VALUES
EC2_JSON_DIR
EC2_METADATA_ADDR
EC2_SUPPORTED_VERSIONS

Public Instance Methods

best_api_version() click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 77
def best_api_version
  response = http_client.get("/")
  unless response.code == '200'
    raise "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")
  versions = response.body.split("\n").sort
  until (versions.empty? || EC2_SUPPORTED_VERSIONS.include?(versions.last)) do
    pv = versions.pop
    Ohai::Log.debug("EC2 shows unsupported metadata version: #{pv}") unless pv == 'latest'
  end
  Ohai::Log.debug("EC2 metadata version: #{versions.last}")
  if versions.empty?
    raise "Unable to determine EC2 metadata version (no supported entries found)"
  end
  versions.last
end
can_metadata_connect?(addr, port, timeout=2) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 51
def can_metadata_connect?(addr, port, timeout=2)
  t = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
  saddr = Socket.pack_sockaddr_in(port, addr)
  connected = false

  begin
    t.connect_nonblock(saddr)
  rescue Errno::EINPROGRESS
    r,w,e = IO::select(nil,[t],nil,timeout)
    if !w.nil?
      connected = true
    else
      begin
        t.connect_nonblock(saddr)
      rescue Errno::EISCONN
        t.close
        connected = true
      rescue SystemCallError
      end
    end
  rescue SystemCallError
  end
  Ohai::Log.debug("can_metadata_connect? == #{connected}")
  connected
end
fetch_dir_metadata(id, api_version) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 155
def fetch_dir_metadata(id, api_version)
  metadata = Hash.new
  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 ? retr_meta : ''
      elsif not key.eql?('/')
        metadata[key[0..-2]] = fetch_dir_metadata("#{id}#{key}", api_version)
      end
    end
    metadata
  end
end
fetch_json_dir_metadata(id, api_version) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 172
def fetch_json_dir_metadata(id, api_version)
  metadata = Hash.new
  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 ? retr_meta : ''
        json = StringIO.new(data)
        parser = FFI_Yajl::Parser.new
        metadata[metadata_key(key)] = parser.parse(json)
      elsif not key.eql?('/')
        metadata[key[0..-2]] = fetch_json_dir_metadata("#{id}#{key}", api_version)
      end
    end
    metadata
  end
end
fetch_metadata(id='', api_version=nil) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 122
def fetch_metadata(id='', api_version=nil)
  api_version ||= best_api_version
  return Hash.new if api_version.nil?

  metadata = Hash.new
  retrieved_metadata = metadata_get(id, 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, api_version)
            retr_meta ? retr_meta.split("\n") : retr_meta
          else
            metadata_get(key, api_version)
          end
      elsif not key.eql?(id) and not key.eql?('/')
        name = key[0..-2]
        sym = metadata_key(name)
        if EC2_ARRAY_DIR.include?(name)
          metadata[sym] = fetch_dir_metadata(key, api_version)
        elsif EC2_JSON_DIR.include?(name)
          metadata[sym] = fetch_json_dir_metadata(key, api_version)
        else
          fetch_metadata(key, api_version).each{|k,v| metadata[k] = v}
        end
      end
    end
    metadata
  end
end
fetch_userdata() click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 192
def fetch_userdata()
  api_version = best_api_version
  return nil if api_version.nil?
  response = http_client.get("/#{api_version}/user-data/")
  response.code == "200" ? response.body : nil
end
http_client() click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 97
def http_client
  Net::HTTP.start(EC2_METADATA_ADDR).tap {|h| h.read_timeout = 600}
end
metadata_get(id, api_version) click to toggle source

Get metadata for a given path and API version

@details

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.
# File lib/ohai/mixin/ec2_metadata.rb, line 108
def metadata_get(id, api_version)
  path = "/#{api_version}/meta-data/#{id}"
  response = http_client.get(path)
  case response.code
  when '200'
    response.body
  when '404'
    Ohai::Log.debug("Encountered 404 response retreiving EC2 metadata path: #{path} ; continuing.")
    nil
  else
    raise "Encountered error retrieving EC2 metadata (#{path} returned #{response.code} response)"
  end
end

Private Instance Methods

expand_path(file_name) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 201
def expand_path(file_name)
  path = file_name.gsub(/\=.*$/, '/')
  # ignore "./" and "../"
  path.gsub(%r{/\.\.?(?:/|$)}, '/').
    sub(%r{^\.\.?(?:/|$)}, '').
    sub(%r{^$}, '/')
end
metadata_key(key) click to toggle source
# File lib/ohai/mixin/ec2_metadata.rb, line 209
def metadata_key(key)
  key.gsub(/\-|\//, '_')
end