diff options
author | Bryan McLellan <btm@opscode.com> | 2013-04-15 06:28:38 -0700 |
---|---|---|
committer | Bryan McLellan <btm@opscode.com> | 2013-04-15 06:28:38 -0700 |
commit | a82462867180e7b3f20dc9bdbbd3ac5d7a7b41ec (patch) | |
tree | 18f7262cb5a4e5bed52901871ac4e256f74d4e60 | |
parent | 505c11d9c53dc19c8604266608782292eeaf8bef (diff) | |
parent | 99fda9c132e318c851ecfce94690ada8e5434f60 (diff) | |
download | ohai-a82462867180e7b3f20dc9bdbbd3ac5d7a7b41ec.tar.gz |
Merge branch 'OHAI-434'
-rw-r--r-- | lib/ohai/mixin/ec2_metadata.rb | 88 | ||||
-rw-r--r-- | spec/unit/mixin/ec2_metadata_spec.rb | 82 | ||||
-rw-r--r-- | spec/unit/plugins/ec2_spec.rb | 51 | ||||
-rw-r--r-- | spec/unit/plugins/eucalyptus_spec.rb | 21 |
4 files changed, 189 insertions, 53 deletions
diff --git a/lib/ohai/mixin/ec2_metadata.rb b/lib/ohai/mixin/ec2_metadata.rb index 3cef7402..49b4647e 100644 --- a/lib/ohai/mixin/ec2_metadata.rb +++ b/lib/ohai/mixin/ec2_metadata.rb @@ -22,11 +22,28 @@ require 'socket' 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) @@ -57,67 +74,99 @@ module Ohai 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 @@ -138,4 +187,3 @@ module Ohai end end end - diff --git a/spec/unit/mixin/ec2_metadata_spec.rb b/spec/unit/mixin/ec2_metadata_spec.rb new file mode 100644 index 00000000..a91da4c1 --- /dev/null +++ b/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 diff --git a/spec/unit/plugins/ec2_spec.rb b/spec/unit/plugins/ec2_spec.rb index 51f15792..e27788f4 100644 --- a/spec/unit/plugins/ec2_spec.rb +++ b/spec/unit/plugins/ec2_spec.rb @@ -42,21 +42,24 @@ describe Ohai::System, "plugin ec2" do 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")) @@ -71,22 +74,22 @@ describe Ohai::System, "plugin ec2" do 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 @@ -96,16 +99,16 @@ describe Ohai::System, "plugin ec2" do 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 @@ -116,7 +119,7 @@ describe Ohai::System, "plugin ec2" do 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/.") @@ -131,16 +134,16 @@ describe Ohai::System, "plugin ec2" do @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") @@ -164,7 +167,7 @@ describe Ohai::System, "plugin ec2" do @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" @@ -178,16 +181,16 @@ describe Ohai::System, "plugin ec2" do 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('') @@ -195,5 +198,5 @@ describe Ohai::System, "plugin ec2" do File.stub!(:read).with('C:\chef\ohai\hints/rackspace.json').and_return('') end end - + end diff --git a/spec/unit/plugins/eucalyptus_spec.rb b/spec/unit/plugins/eucalyptus_spec.rb index d78b9951..afe80ed4 100644 --- a/spec/unit/plugins/eucalyptus_spec.rb +++ b/spec/unit/plugins/eucalyptus_spec.rb @@ -39,17 +39,20 @@ describe Ohai::System, "plugin eucalyptus" do @ohai.stub!(:http_client).and_return(@http_client) @http_client.should_receive(:get). + with("/").twice. + and_return(mock("Net::HTTP Response", :body => "2012-01-12", :code => "200")) + @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")) @@ -84,7 +87,7 @@ describe Ohai::System, "plugin eucalyptus" do @ohai[:network] = { "interfaces" => { "eth0" => { "addresses" => { "ff:ff:95:47:6E:ED"=> { "family" => "lladdr" } } } } } end end - + describe "with eucalyptus cloud file" do it_should_behave_like "eucalyptus" @@ -98,16 +101,16 @@ describe Ohai::System, "plugin eucalyptus" do describe "without cloud file" do it_should_behave_like "!eucalyptus" - + before(:each) do File.stub!(:exist?).with('/etc/chef/ohai/hints/eucalyptus.json').and_return(false) File.stub!(:exist?).with('C:\chef\ohai\hints/eucalyptus.json').and_return(false) end end - + describe "with ec2 cloud file" do it_should_behave_like "!eucalyptus" - + before(:each) do File.stub!(:exist?).with('/etc/chef/ohai/hints/ec2.json').and_return(true) File.stub!(:read).with('/etc/chef/ohai/hints/ec2.json').and_return('') @@ -115,5 +118,5 @@ describe Ohai::System, "plugin eucalyptus" do File.stub!(:read).with('C:\chef\ohai\hints/ec2.json').and_return('') end end - + end |