Skip to content

Commit

Permalink
Merge branch 'OHAI-434'
Browse files Browse the repository at this point in the history
  • Loading branch information
btm committed Apr 15, 2013
2 parents 505c11d + 99fda9c commit a824628
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 53 deletions.
88 changes: 68 additions & 20 deletions lib/ohai/mixin/ec2_metadata.rb
Expand Up @@ -22,11 +22,28 @@

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" unless defined?(EC2_METADATA_ADDR)
EC2_METADATA_URL = "/2012-01-12/meta-data" unless defined?(EC2_METADATA_URL)
EC2_USERDATA_URL = "/2012-01-12/user-data" unless defined?(EC2_USERDATA_URL)
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 ]

EC2_ARRAY_VALUES = %w(security-groups)
EC2_ARRAY_DIR = %w(network/interfaces/macs)
EC2_JSON_DIR = %w(iam)
Expand Down Expand Up @@ -57,67 +74,99 @@ def can_metadata_connect?(addr, port, timeout=2)
connected
end

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

def http_client
Net::HTTP.start(EC2_METADATA_ADDR).tap {|h| h.read_timeout = 600}
end

def fetch_metadata(id='')
def metadata_get(id, api_version)
response = http_client.get("/#{api_version}/meta-data/#{id}")
unless response.code == '200'
raise "Encountered error retrieving EC2 metadata (returned #{response.code} response)"
end
response
end

def fetch_metadata(id='', api_version=nil)
api_version ||= best_api_version
return Hash.new if api_version.nil?
metadata = Hash.new
http_client.get("#{EC2_METADATA_URL}/#{id}").body.split("\n").each do |o|
metadata_get(id, api_version).body.split("\n").each do |o|
key = expand_path("#{id}#{o}")
if key[-1..-1] != '/'
metadata[metadata_key(key)] =
if EC2_ARRAY_VALUES.include? key
http_client.get("#{EC2_METADATA_URL}/#{key}").body.split("\n")
metadata_get(key, api_version).body.split("\n")
else
http_client.get("#{EC2_METADATA_URL}/#{key}").body
metadata_get(key, api_version).body
end
elsif not key.eql?(id) and not key.eql?('/')
name = key[0..-2]
sym = metadata_key(name)
sym = metadata_key(name)
if EC2_ARRAY_DIR.include?(name)
metadata[sym] = fetch_dir_metadata(key)
metadata[sym] = fetch_dir_metadata(key, api_version)
elsif EC2_JSON_DIR.include?(name)
metadata[sym] = fetch_json_dir_metadata(key)
metadata[sym] = fetch_json_dir_metadata(key, api_version)
else
fetch_metadata(key).each{|k,v| metadata[k] = v}
fetch_metadata(key, api_version).each{|k,v| metadata[k] = v}
end
end
end
metadata
end

def fetch_dir_metadata(id)
def fetch_dir_metadata(id, api_version)
metadata = Hash.new
http_client.get("#{EC2_METADATA_URL}/#{id}").body.split("\n").each do |o|
metadata_get(id, api_version).body.split("\n").each do |o|
key = expand_path(o)
if key[-1..-1] != '/'
metadata[metadata_key(key)] = http_client.get("#{EC2_METADATA_URL}/#{id}#{key}").body
metadata[metadata_key(key)] = metadata_get("#{id}#{key}", api_version).body
elsif not key.eql?('/')
metadata[key[0..-2]] = fetch_dir_metadata("#{id}#{key}")
metadata[key[0..-2]] = fetch_dir_metadata("#{id}#{key}", api_version)
end
end
metadata
end

def fetch_json_dir_metadata(id)
def fetch_json_dir_metadata(id, api_version)
metadata = Hash.new
http_client.get("#{EC2_METADATA_URL}/#{id}").body.split("\n").each do |o|
metadata_get(id, api_version).body.split("\n").each do |o|
key = expand_path(o)
if key[-1..-1] != '/'
data = http_client.get("#{EC2_METADATA_URL}/#{id}#{key}").body
data = metadata_get("#{id}#{key}", api_version).body
json = StringIO.new(data)
parser = Yajl::Parser.new
metadata[metadata_key(key)] = parser.parse(json)
elsif not key.eql?('/')
metadata[key[0..-2]] = fetch_json_dir_metadata("#{id}#{key}")
metadata[key[0..-2]] = fetch_json_dir_metadata("#{id}#{key}", api_version)
end
end
metadata
end

def fetch_userdata()
response = http_client.get("#{EC2_USERDATA_URL}/")
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

Expand All @@ -138,4 +187,3 @@ def metadata_key(key)
end
end
end

82 changes: 82 additions & 0 deletions spec/unit/mixin/ec2_metadata_spec.rb
@@ -0,0 +1,82 @@
#
# Author:: Bryan McLellan <btm@loftninjas.org>
# Copyright:: Copyright (c) 2013 Opscode, 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 CONDIT"Net::HTTP Response"NS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
require 'ohai/mixin/ec2_metadata'

describe Ohai::Mixin::Ec2Metadata do
let(:mixin) {
metadata_object = Object.new.extend(Ohai::Mixin::Ec2Metadata)
http_client = mock("Net::HTTP client")
http_client.stub!(:get).and_return(response)
metadata_object.stub!(:http_client).and_return(http_client)
metadata_object
}

context "#best_api_version" do
context "with a sorted list of metadata versions" do
let(:response) { mock("Net::HTTP Response", :body => "1.0\n2011-05-01\n2012-01-12\nUnsupported", :code => "200") }

it "returns the most recent version" do
mixin.best_api_version.should == "2012-01-12"
end
end

context "with an unsorted list of metadata versions" do
let(:response) { mock("Net::HTTP Response", :body => "1.0\n2009-04-04\n2007-03-01\n2011-05-01\n2008-09-01\nUnsupported", :code => "200") }

it "returns the most recent version (using string sort)" do
mixin.best_api_version.should == "2011-05-01"
end
end

context "when no supported versions are found" do
let(:response) { mock("Net::HTTP Response", :body => "2020-01-01\nUnsupported", :code => "200") }

it "raises an error" do
lambda { mixin.best_api_version}.should raise_error
end
end

context "when the response code is 404" do
let(:response) { mock("Net::HTTP Response", :body => "1.0\n2011-05-01\n2012-01-12\nUnsupported", :code => "404") }

it "raises an error" do
lambda { mixin.best_api_version}.should raise_error
end
end

context "when the response code is unexpected" do
let(:response) { mock("Net::HTTP Response", :body => "1.0\n2011-05-01\n2012-01-12\nUnsupported", :code => "418") }

it "raises an error" do
lambda { mixin.best_api_version}.should raise_error
end
end
end

context "#metadata_get" do
context "when the response code is unexpected" do
let(:response) { mock("Net::HTTP Response", :body => "", :code => "418") }

it "raises an error" do
lambda { mixin.metadata_get('', '2012-01-12') }.should raise_error(RuntimeError)
end
end
end
end
51 changes: 27 additions & 24 deletions spec/unit/plugins/ec2_spec.rb
Expand Up @@ -42,21 +42,24 @@
t = mock("connection")
t.stub!(:connect_nonblock).and_raise(Errno::EINPROGRESS)
Socket.stub!(:new).and_return(t)
@http_client.should_receive(:get).
with("/").twice.
and_return(mock("Net::HTTP Response", :body => "2012-01-12", :code => "200"))
end

it "should recursively fetch all the ec2 metadata" do
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/").
and_return(mock("Net::HTTP Response", :body => "instance_type\nami_id\nsecurity-groups"))
and_return(mock("Net::HTTP Response", :body => "instance_type\nami_id\nsecurity-groups", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/instance_type").
and_return(mock("Net::HTTP Response", :body => "c1.medium"))
and_return(mock("Net::HTTP Response", :body => "c1.medium", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/ami_id").
and_return(mock("Net::HTTP Response", :body => "ami-5d2dc934"))
and_return(mock("Net::HTTP Response", :body => "ami-5d2dc934", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/security-groups").
and_return(mock("Net::HTTP Response", :body => "group1\ngroup2"))
and_return(mock("Net::HTTP Response", :body => "group1\ngroup2", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/user-data/").
and_return(mock("Net::HTTP Response", :body => "By the pricking of my thumb...", :code => "200"))
Expand All @@ -71,22 +74,22 @@
it "should parse ec2 network/ directory as a multi-level hash" do
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/").
and_return(mock("Net::HTTP Response", :body => "network/"))
and_return(mock("Net::HTTP Response", :body => "network/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/network/").
and_return(mock("Net::HTTP Response", :body => "interfaces/"))
and_return(mock("Net::HTTP Response", :body => "interfaces/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/network/interfaces/").
and_return(mock("Net::HTTP Response", :body => "macs/"))
and_return(mock("Net::HTTP Response", :body => "macs/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/network/interfaces/macs/").
and_return(mock("Net::HTTP Response", :body => "12:34:56:78:9a:bc/"))
and_return(mock("Net::HTTP Response", :body => "12:34:56:78:9a:bc/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/network/interfaces/macs/12:34:56:78:9a:bc/").
and_return(mock("Net::HTTP Response", :body => "public_hostname"))
and_return(mock("Net::HTTP Response", :body => "public_hostname", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/network/interfaces/macs/12:34:56:78:9a:bc/public_hostname").
and_return(mock("Net::HTTP Response", :body => "server17.opscode.com"))
and_return(mock("Net::HTTP Response", :body => "server17.opscode.com", :code => "200"))
@ohai._require_plugin("ec2")

@ohai[:ec2].should_not be_nil
Expand All @@ -96,16 +99,16 @@
it "should parse ec2 iam/ directory and its JSON files properly" do
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/").
and_return(mock("Net::HTTP Response", :body => "iam/"))
and_return(mock("Net::HTTP Response", :body => "iam/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/iam/").
and_return(mock("Net::HTTP Response", :body => "security-credentials/"))
and_return(mock("Net::HTTP Response", :body => "security-credentials/", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/iam/security-credentials/").
and_return(mock("Net::HTTP Response", :body => "MyRole"))
and_return(mock("Net::HTTP Response", :body => "MyRole", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/iam/security-credentials/MyRole").
and_return(mock("Net::HTTP Response", :body => "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2012-08-22T07:47:22Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"AAAAAAAA\",\n \"SecretAccessKey\" : \"SSSSSSSS\",\n \"Token\" : \"12345678\",\n \"Expiration\" : \"2012-08-22T11:25:52Z\"\n}"))
and_return(mock("Net::HTTP Response", :body => "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2012-08-22T07:47:22Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"AAAAAAAA\",\n \"SecretAccessKey\" : \"SSSSSSSS\",\n \"Token\" : \"12345678\",\n \"Expiration\" : \"2012-08-22T11:25:52Z\"\n}", :code => "200"))
@ohai._require_plugin("ec2")

@ohai[:ec2].should_not be_nil
Expand All @@ -116,7 +119,7 @@
it "should ignore \"./\" and \"../\" on ec2 metadata paths to avoid infinity loops" do
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/").
and_return(mock("Net::HTTP Response", :body => ".\n./\n..\n../\npath1/.\npath2/./\npath3/..\npath4/../"))
and_return(mock("Net::HTTP Response", :body => ".\n./\n..\n../\npath1/.\npath2/./\npath3/..\npath4/../", :code => "200"))

@http_client.should_not_receive(:get).
with("/2012-01-12/meta-data/.")
Expand All @@ -131,16 +134,16 @@

@http_client.should_receive(:get).
with("/2012-01-12/meta-data/path1/").
and_return(mock("Net::HTTP Response", :body => ""))
and_return(mock("Net::HTTP Response", :body => "", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/path2/").
and_return(mock("Net::HTTP Response", :body => ""))
and_return(mock("Net::HTTP Response", :body => "", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/path3/").
and_return(mock("Net::HTTP Response", :body => ""))
and_return(mock("Net::HTTP Response", :body => "", :code => "200"))
@http_client.should_receive(:get).
with("/2012-01-12/meta-data/path4/").
and_return(mock("Net::HTTP Response", :body => ""))
and_return(mock("Net::HTTP Response", :body => "", :code => "200"))

@ohai._require_plugin("ec2")

Expand All @@ -164,7 +167,7 @@
@ohai[:network][:interfaces][:eth0][:arp] = {"169.254.1.0"=>"00:50:56:c0:00:08"}
end
end

describe "with ec2 cloud file" do
it_should_behave_like "ec2"

Expand All @@ -178,22 +181,22 @@

describe "without cloud file" do
it_should_behave_like "!ec2"

before(:each) do
File.stub!(:exist?).with('/etc/chef/ohai/hints/ec2.json').and_return(false)
File.stub!(:exist?).with('C:\chef\ohai\hints/ec2.json').and_return(false)
end
end

describe "with rackspace cloud file" do
it_should_behave_like "!ec2"

before(:each) do
File.stub!(:exist?).with('/etc/chef/ohai/hints/rackspace.json').and_return(true)
File.stub!(:read).with('/etc/chef/ohai/hints/rackspace.json').and_return('')
File.stub!(:exist?).with('C:\chef\ohai\hints/rackspace.json').and_return(true)
File.stub!(:read).with('C:\chef\ohai\hints/rackspace.json').and_return('')
end
end

end

0 comments on commit a824628

Please sign in to comment.