diff --git a/lib/facter/core/suitable.rb b/lib/facter/core/suitable.rb index 1b3c04fceb..0262470919 100644 --- a/lib/facter/core/suitable.rb +++ b/lib/facter/core/suitable.rb @@ -108,10 +108,6 @@ def weight # # @api private def suitable? - unless defined? @suitable - @suitable = ! @confines.detect { |confine| ! confine.true? } - end - - return @suitable + @confines.all? { |confine| confine.true? } end end diff --git a/lib/facter/ec2.rb b/lib/facter/ec2.rb index 09e0109528..7afd97d86f 100644 --- a/lib/facter/ec2.rb +++ b/lib/facter/ec2.rb @@ -1,37 +1,45 @@ require 'facter/util/ec2' -require 'open-uri' -def metadata(id = "") - open("http://169.254.169.254/2008-02-01/meta-data/#{id||=''}").read. - split("\n").each do |o| - key = "#{id}#{o.gsub(/\=.*$/, '/')}" - if key[-1..-1] != '/' - value = open("http://169.254.169.254/2008-02-01/meta-data/#{key}").read. - split("\n") - symbol = "ec2_#{key.gsub(/\-|\//, '_')}".to_sym - Facter.add(symbol) { setcode { value.join(',') } } - else - metadata(key) +Facter.define_fact(:ec2_metadata) do + define_resolution(:rest) do + confine do + Facter.value(:virtual).match /^xen/ + end + + confine do + Facter::Util::EC2.uri_reachable?("http://169.254.169.254/latest/meta-data/") + end + + setcode do + metadata_uri = "http://169.254.169.254/latest/meta-data/" + Facter::Util::EC2.recursive_fetch(metadata_uri) end end -rescue => details - Facter.warn "Could not retrieve ec2 metadata: #{details.message}" end -def userdata() - Facter.add(:ec2_userdata) do +Facter.define_fact(:ec2_userdata) do + define_resolution(:rest) do + confine do + Facter.value(:virtual).match /^xen/ + end + + confine do + Facter::Util::EC2.uri_reachable?("http://169.254.169.254/latest/user-data/") + end + setcode do - if userdata = Facter::Util::EC2.userdata - userdata.split - end + userdata_uri = "http://169.254.169.254/latest/user-data/" + output = Facter::Util::EC2.fetch(userdata_uri) + output.join("\n") end end end -if (Facter::Util::EC2.has_euca_mac? || Facter::Util::EC2.has_openstack_mac? || - Facter::Util::EC2.has_ec2_arp?) && Facter::Util::EC2.can_connect? - metadata - userdata -else - Facter.debug "Not an EC2 host" +# The flattened version of the EC2 facts are deprecated and will be removed in +# a future release of Facter. +if (ec2_metadata = Facter.value(:ec2_metadata)) + ec2_facts = Facter::Util::Values.flatten_structure("ec2", ec2_metadata) + ec2_facts.each_pair do |factname, factvalue| + Facter.add(factname, :value => factvalue) + end end diff --git a/lib/facter/util/ec2.rb b/lib/facter/util/ec2.rb index c6e5dcadd4..36c32f8488 100644 --- a/lib/facter/util/ec2.rb +++ b/lib/facter/util/ec2.rb @@ -3,99 +3,99 @@ # Provide a set of utility static methods that help with resolving the EC2 # fact. +# +# @see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html module Facter::Util::EC2 - class << self - # Test if we can connect to the EC2 api. Return true if able to connect. - # On failure this function fails silently and returns false. - # - # The +wait_sec+ parameter provides you with an adjustable timeout. - # - def can_connect?(wait_sec=2) - url = "http://169.254.169.254:80/" - Timeout::timeout(wait_sec) {open(url)} - return true - rescue Timeout::Error - return false - rescue - return false - end + CONNECTION_ERRORS = [ + Errno::EHOSTDOWN, + Errno::EHOSTUNREACH, + Errno::ENETUNREACH, + Errno::ECONNABORTED, + Errno::ECONNREFUSED, + Errno::ECONNRESET, + Errno::ETIMEDOUT, + ] - # Test if this host has a mac address used by Eucalyptus clouds, which - # normally is +d0:0d+. - def has_euca_mac? - !!(Facter.value(:macaddress) =~ %r{^[dD]0:0[dD]:}) - end + # Query a specific AWS metadata URI. + # + # @api private + def self.fetch(uri) + body = open(uri).read - # Test if this host has a mac address used by OpenStack, which - # normally starts with FA:16:3E (older versions of OpenStack - # may generate mac addresses starting with 02:16:3E) - def has_openstack_mac? - !!(Facter.value(:macaddress) =~ %r{^(02|[fF][aA]):16:3[eE]}) + lines = body.split("\n").map do |line| + if (match = line.match(/^(\d+)=.*$/)) + # Metadata arrays are formatted like '=/', so + # we need to extract the index from that output. + "#{match[1]}/" + else + line + end end - # Test if the host has an arp entry in its cache that matches the EC2 arp, - # which is normally +fe:ff:ff:ff:ff:ff+. - def has_ec2_arp? - kernel = Facter.value(:kernel) + lines + rescue OpenURI::HTTPError => e + if e.message.match /404 Not Found/i + return nil + else + Facter.log_exception(e, "Failed to fetch ec2 uri #{uri}: #{e.message}") + return nil + end + rescue *CONNECTION_ERRORS => e + Facter.log_exception(e, "Failed to fetch ec2 uri #{uri}: #{e.message}") + end - mac_address_re = case kernel - when /Windows/i - /fe-ff-ff-ff-ff-ff/i - else - /fe:ff:ff:ff:ff:ff/i - end + def self.recursive_fetch(uri) + results = {} - arp_command = case kernel - when /Windows/i, /SunOS/i - "arp -a" - else - "arp -an" - end + keys = fetch(uri) - if arp_table = Facter::Core::Execution.exec(arp_command) - return true if arp_table.match(mac_address_re) + keys.each do |key| + if key.match(%r[/$]) + # If a metadata key is suffixed with '/' then it's a general metadata + # resource, so we have to recursively look up all the keys in the given + # collection. + name = key[0..-2] + results[name] = recursive_fetch("#{uri}#{key}") + else + # This is a simple key/value pair, we can just query the given endpoint + # and store the results. + ret = fetch("#{uri}#{key}") + results[key] = ret.size > 1 ? ret : ret.first end - return false end + + results end - ## - # userdata returns a single string containing the body of the response of the - # GET request for the URI http://169.254.169.254/latest/user-data/ If the - # metadata server responds with a 404 Not Found error code then this method - # retuns `nil`. - # - # @param version [String] containing the API version for the request. - # Defaults to "latest" and other examples are documented at - # http://aws.amazon.com/archives/Amazon%20EC2 + # Is the given URI reachable? # - # @api public + # @param uri [String] The HTTP URI to attempt to reach # - # @return [String] containing the response body or `nil` - def self.userdata(version="latest") - uri = "http://169.254.169.254/#{version}/user-data/" + # @return [true, false] If the given URI could be fetched after retry_limit attempts + def self.uri_reachable?(uri, retry_limit = 3) + timeout = 0.2 + able_to_connect = false + attempts = 0 + begin - read_uri(uri) - rescue OpenURI::HTTPError => detail - case detail.message - when /404 Not Found/i - Facter.debug "No user-data present at #{uri}: server responded with #{detail.message}" - return nil + Timeout.timeout(timeout) do + open(uri).read + end + able_to_connect = true + rescue OpenURI::HTTPError => e + if e.message.match /404 Not Found/i + able_to_connect = false else - raise detail + retry if attempts < retry_limit end + rescue Timeout::Error + retry if attempts < retry_limit + rescue *CONNECTION_ERRORS + retry if attempts < retry_limit + ensure + attempts = attempts + 1 end - end - ## - # read_uri provides a seam method to easily test the HTTP client - # functionality of a HTTP based metadata server. - # - # @api private - # - # @return [String] containing the body of the response - def self.read_uri(uri) - open(uri).read + able_to_connect end - private_class_method :read_uri end diff --git a/lib/facter/util/resolution.rb b/lib/facter/util/resolution.rb index 0182a9ae63..9b3efa097f 100644 --- a/lib/facter/util/resolution.rb +++ b/lib/facter/util/resolution.rb @@ -62,6 +62,10 @@ def initialize(name, fact) @weight = nil end + def resolution_type + :simple + end + # Evaluate the given block in the context of this resolution. If a block has # already been evaluated emit a warning to that effect. # diff --git a/lib/facter/util/values.rb b/lib/facter/util/values.rb index a7048d54da..1fbb1b686e 100644 --- a/lib/facter/util/values.rb +++ b/lib/facter/util/values.rb @@ -1,3 +1,4 @@ + module Facter module Util # A util module for facter containing helper methods @@ -75,6 +76,34 @@ def convert(value) value = value.downcase if value.is_a?(String) value end + + # Flatten the given data structure to something that's suitable to return + # as flat facts. + # + # @param path [String] The fact path to be prefixed to the given value. + # @param structure [Object] The data structure to flatten. Nested hashes + # will be recursively flattened, everything else will be returned as-is. + # + # @return [Hash] The given data structure prefixed with the given path + def flatten_structure(path, structure) + results = {} + + if structure.is_a? Hash + structure.each_pair do |name, value| + new_path = "#{path}_#{name}".gsub(/\-|\//, '_') + results.merge! flatten_structure(new_path, value) + end + elsif structure.is_a? Array + structure.each_with_index do |value, index| + new_path = "#{path}_#{index}" + results.merge! flatten_structure(new_path, value) + end + else + results[path] = structure + end + + results + end end end end diff --git a/spec/fixtures/unit/util/ec2/meta-data/root b/spec/fixtures/unit/util/ec2/meta-data/root new file mode 100644 index 0000000000..9ec3bbedad --- /dev/null +++ b/spec/fixtures/unit/util/ec2/meta-data/root @@ -0,0 +1,20 @@ +ami-id +ami-launch-index +ami-manifest-path +block-device-mapping/ +hostname +instance-action +instance-id +instance-type +kernel-id +local-hostname +local-ipv4 +mac +metrics/ +network/ +placement/ +profile +public-hostname +public-ipv4 +public-keys/ +reservation-id diff --git a/spec/unit/core/suitable_spec.rb b/spec/unit/core/suitable_spec.rb index 4c0b1fde06..8277408799 100644 --- a/spec/unit/core/suitable_spec.rb +++ b/spec/unit/core/suitable_spec.rb @@ -92,5 +92,15 @@ def initialize expect(subject).to_not be_suitable end + + it "recalculates suitability on every invocation" do + subject.confine :kernel => 'Linux' + + subject.confines.first.stubs(:true?).returns false + expect(subject).to_not be_suitable + subject.confines.first.unstub(:true?) + subject.confines.first.stubs(:true?).returns true + expect(subject).to be_suitable + end end end diff --git a/spec/unit/ec2_spec.rb b/spec/unit/ec2_spec.rb index f26a61316f..a206e898af 100755 --- a/spec/unit/ec2_spec.rb +++ b/spec/unit/ec2_spec.rb @@ -1,187 +1,126 @@ -#! /usr/bin/env ruby - require 'spec_helper' require 'facter/util/ec2' -describe "ec2 facts" do - # This is the standard prefix for making an API call in EC2 (or fake) - # environments. - let(:api_prefix) { "http://169.254.169.254" } - - describe "when running on ec2" do - before :each do - # This is an ec2 instance, not a eucalyptus instance - Facter::Util::EC2.stubs(:has_euca_mac?).returns(false) - Facter::Util::EC2.stubs(:has_openstack_mac?).returns(false) - Facter::Util::EC2.stubs(:has_ec2_arp?).returns(true) - - # Assume we can connect - Facter::Util::EC2.stubs(:can_connect?).returns(true) - end - - it "should create flat meta-data facts" do - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/"). - at_least_once.returns(StringIO.new("foo")) - - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/foo"). - at_least_once.returns(StringIO.new("bar")) - - Facter.collection.internal_loader.load(:ec2) - - Facter.fact(:ec2_foo).value.should == "bar" - end +describe "ec2_metadata" do + before do + Facter.collection.internal_loader.load(:ec2) + end - it "should create flat meta-data facts with comma seperation" do - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/"). - at_least_once.returns(StringIO.new("foo")) + subject { Facter.fact(:ec2_metadata).resolution(:rest) } - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/foo"). - at_least_once.returns(StringIO.new("bar\nbaz")) + it "is unsuitable if the virtual fact is not xen" do + Facter.fact(:virtual).stubs(:value).returns "kvm" + Facter::Util::EC2.stubs(:uri_reachable?).returns true + expect(subject).to_not be_suitable + end - Facter.collection.internal_loader.load(:ec2) + it "is unsuitable if ec2 endpoint is not reachable" do + Facter.fact(:virtual).stubs(:value).returns "xen" + Facter::Util::EC2.stubs(:uri_reachable?).returns false + expect(subject).to_not be_suitable + end - Facter.fact(:ec2_foo).value.should == "bar,baz" + describe "when the ec2 endpoint is reachable" do + before do + Facter::Util::EC2.stubs(:uri_reachable?).returns true end - it "should create structured meta-data facts" do - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/"). - at_least_once.returns(StringIO.new("foo/")) - - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/foo/"). - at_least_once.returns(StringIO.new("bar")) - - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/foo/bar"). - at_least_once.returns(StringIO.new("baz")) - - Facter.collection.internal_loader.load(:ec2) - - Facter.fact(:ec2_foo_bar).value.should == "baz" + it "is suitable if the virtual fact is xen" do + Facter.fact(:virtual).stubs(:value).returns "xen" + expect(subject).to be_suitable end - it "should create ec2_user_data fact" do - # No meta-data - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/"). - at_least_once.returns(StringIO.new("")) - - Facter::Util::EC2.stubs(:read_uri). - with("#{api_prefix}/latest/user-data/"). - returns("test") - - Facter.collection.internal_loader.load(:ec2) - Facter.fact(:ec2_userdata).value.should == ["test"] + it "is suitable if the virtual fact is xenu" do + Facter.fact(:virtual).stubs(:value).returns "xenu" + expect(subject).to be_suitable end end - describe "when running on eucalyptus" do - before :each do - # Return false for ec2, true for eucalyptus - Facter::Util::EC2.stubs(:has_euca_mac?).returns(true) - Facter::Util::EC2.stubs(:has_openstack_mac?).returns(false) - Facter::Util::EC2.stubs(:has_ec2_arp?).returns(false) - - # Assume we can connect - Facter::Util::EC2.stubs(:can_connect?).returns(true) - end + let(:metadata_root) { 'http://169.254.169.254/latest/meta-data/' } - it "should create ec2_user_data fact" do - # No meta-data - Object.any_instance.expects(:open).\ - with("#{api_prefix}/2008-02-01/meta-data/").\ - at_least_once.returns(StringIO.new("")) + it "resolves the value by recursively querying the rest endpoint" do + Facter::Util::EC2.expects(:recursive_fetch).with(metadata_root).returns({"hello" => "world"}) + expect(subject.value).to eq({"hello" => "world"}) + end +end - Facter::Util::EC2.stubs(:read_uri). - with("#{api_prefix}/latest/user-data/"). - returns("test") +describe "ec2_userdata" do + before do + Facter.collection.internal_loader.load(:ec2) + end - # Force a fact load - Facter.collection.internal_loader.load(:ec2) + subject { Facter.fact(:ec2_userdata).resolution(:rest) } - Facter.fact(:ec2_userdata).value.should == ["test"] - end + it "is unsuitable if the virtual fact is not xen" do + Facter.fact(:virtual).stubs(:value).returns "kvm" + Facter::Util::EC2.stubs(:uri_reachable?).returns true + expect(subject).to_not be_suitable end - describe "when running on openstack" do - before :each do - # Return false for ec2, true for eucalyptus - Facter::Util::EC2.stubs(:has_openstack_mac?).returns(true) - Facter::Util::EC2.stubs(:has_euca_mac?).returns(false) - Facter::Util::EC2.stubs(:has_ec2_arp?).returns(false) + it "is unsuitable if ec2 endpoint is not reachable" do + Facter.fact(:virtual).stubs(:value).returns "xen" + Facter::Util::EC2.stubs(:uri_reachable?).returns false + expect(subject).to_not be_suitable + end - # Assume we can connect - Facter::Util::EC2.stubs(:can_connect?).returns(true) + describe "when the ec2 endpoint is reachable" do + before do + Facter::Util::EC2.stubs(:uri_reachable?).returns true end - it "should create ec2_user_data fact" do - # No meta-data - Object.any_instance.expects(:open).\ - with("#{api_prefix}/2008-02-01/meta-data/").\ - at_least_once.returns(StringIO.new("")) - - Facter::Util::EC2.stubs(:read_uri). - with("#{api_prefix}/latest/user-data/"). - returns("test") - - # Force a fact load - Facter.collection.internal_loader.load(:ec2) - - Facter.fact(:ec2_userdata).value.should == ["test"] + it "is suitable if the virtual fact is xen" do + Facter.fact(:virtual).stubs(:value).returns "xen" + expect(subject).to be_suitable end - it "should return nil if open fails" do - Facter.stubs(:warn) # do not pollute test output - Facter.expects(:warn).with('Could not retrieve ec2 metadata: host unreachable') - - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/"). - at_least_once.raises(RuntimeError, 'host unreachable') + it "is suitable if the virtual fact is xenu" do + Facter.fact(:virtual).stubs(:value).returns "xenu" + expect(subject).to be_suitable + end + end - Facter::Util::EC2.stubs(:read_uri). - with("#{api_prefix}/latest/user-data/"). - raises(RuntimeError, 'host unreachable') + let(:userdata_uri) { 'http://169.254.169.254/latest/user-data/' } - # Force a fact load - Facter.collection.internal_loader.load(:ec2) + it "resolves the value by fetching the rest endpoint" do + Facter::Util::EC2.expects(:fetch).with(userdata_uri).returns(["my ec2 userdata"]) + expect(subject.value).to eq "my ec2 userdata" + end - Facter.fact(:ec2_userdata).value.should be_nil - end + it "returns the user data as a single string" do + userdata = [ + "my multiline", + "user data", + "split by fetch" + ] + Facter::Util::EC2.expects(:fetch).with(userdata_uri).returns(userdata) + expect(subject.value).to eq "my multiline\nuser data\nsplit by fetch" end +end - describe "when api connect test fails" do - before :each do - Facter.stubs(:warnonce) - end +describe "flattened versions of ec2 facts" do + # These facts are tricky to test because they are dynamic facts, and they are + # generated from a fact that is defined in the same file. In order to pull + # this off we need to define the ec2_metadata fact ahead of time so that we + # can stub the value, and then manually load the correct files. - it "should not populate ec2_userdata" do - # Emulate ec2 for now as it matters little to this test - Facter::Util::EC2.stubs(:has_euca_mac?).returns(true) - Facter::Util::EC2.stubs(:has_ec2_arp?).never - Facter::Util::EC2.expects(:can_connect?).at_least_once.returns(false) + it "unpacks the ec2_metadata fact" do + Facter.define_fact(:ec2_metadata).stubs(:value).returns({"hello" => "world"}) + Facter.collection.internal_loader.load(:ec2) - # The API should never be called at this point - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/meta-data/").never - Object.any_instance.expects(:open). - with("#{api_prefix}/2008-02-01/user-data/").never + expect(Facter.value("ec2_hello")).to eq "world" + end - # Force a fact load - Facter.collection.internal_loader.load(:ec2) + it "does not set any flat ec2 facts if the ec2_metadata fact is nil" do + Facter.define_fact(:ec2_metadata).stubs(:value) + Facter.define_fact(:ec2_userdata).stubs(:value).returns(nil) - Facter.fact(:ec2_userdata).should == nil - end + Facter.collection.internal_loader.load(:ec2) - it "should rescue the exception" do - Facter::Util::EC2.expects(:open).with("#{api_prefix}:80/").raises(Timeout::Error) + all_facts = Facter.collection.to_hash - Facter::Util::EC2.should_not be_can_connect - end + ec2_facts = all_facts.keys.select { |k| k =~ /^ec2_/ } + expect(ec2_facts).to be_empty end + end diff --git a/spec/unit/util/ec2_spec.rb b/spec/unit/util/ec2_spec.rb index f963db6c32..201b2beb61 100755 --- a/spec/unit/util/ec2_spec.rb +++ b/spec/unit/util/ec2_spec.rb @@ -1,180 +1,104 @@ -#! /usr/bin/env ruby - require 'spec_helper' require 'facter/util/ec2' describe Facter::Util::EC2 do - # This is the standard prefix for making an API call in EC2 (or fake) - # environments. - let(:api_prefix) { "http://169.254.169.254" } - - describe "is_ec2_arp? method" do - describe "on linux" do - before :each do - # Return fake kernel - Facter.stubs(:value).with(:kernel).returns("linux") - end - - it "should succeed if arp table contains fe:ff:ff:ff:ff:ff" do - ec2arp = my_fixture_read("linux-arp-ec2.out") - Facter::Core::Execution.expects(:exec).with("arp -an").\ - at_least_once.returns(ec2arp) - Facter::Util::EC2.has_ec2_arp?.should == true - end - - it "should succeed if arp table contains FE:FF:FF:FF:FF:FF" do - ec2arp = my_fixture_read("centos-arp-ec2.out") - Facter::Core::Execution.expects(:exec).with("arp -an").\ - at_least_once.returns(ec2arp) - Facter::Util::EC2.has_ec2_arp?.should == true - end - - it "should fail if arp table does not contain fe:ff:ff:ff:ff:ff" do - ec2arp = my_fixture_read("linux-arp-not-ec2.out") - Facter::Core::Execution.expects(:exec).with("arp -an"). - at_least_once.returns(ec2arp) - Facter::Util::EC2.has_ec2_arp?.should == false - end - end - - describe "on windows" do - before :each do - # Return fake kernel - Facter.stubs(:value).with(:kernel).returns("windows") - end - - it "should succeed if arp table contains fe-ff-ff-ff-ff-ff" do - ec2arp = my_fixture_read("windows-2008-arp-a.out") - Facter::Core::Execution.expects(:exec).with("arp -a").\ - at_least_once.returns(ec2arp) - Facter::Util::EC2.has_ec2_arp?.should == true - end - - it "should fail if arp table does not contain fe-ff-ff-ff-ff-ff" do - ec2arp = my_fixture_read("windows-2008-arp-a-not-ec2.out") - Facter::Core::Execution.expects(:exec).with("arp -a"). - at_least_once.returns(ec2arp) - Facter::Util::EC2.has_ec2_arp?.should == false - end - end + let(:response) { StringIO.new } - describe "on solaris" do - before :each do - Facter.stubs(:value).with(:kernel).returns("SunOS") - end + describe "fetching a uri" do + it "splits the body into an array" do + response.string = my_fixture_read("meta-data/root") + described_class.stubs(:open).with("http://169.254.169.254/latest/meta-data/").returns response + output = described_class.fetch("http://169.254.169.254/latest/meta-data/") - it "should fail if arp table does not contain fe:ff:ff:ff:ff:ff" do - ec2arp = my_fixture_read("solaris8_arp_a_not_ec2.out") - - Facter::Core::Execution.expects(:exec).with("arp -a"). - at_least_once.returns(ec2arp) - - Facter::Util::EC2.has_ec2_arp?.should == false - end + expect(output).to eq %w[ + ami-id ami-launch-index ami-manifest-path block-device-mapping/ hostname + instance-action instance-id instance-type kernel-id local-hostname + local-ipv4 mac metrics/ network/ placement/ profile public-hostname + public-ipv4 public-keys/ reservation-id + ] end - end - describe "is_euca_mac? method" do - it "should return true when the mac is a eucalyptus one" do - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("d0:0d:1a:b0:a1:00") + it "reformats keys that are array indices" do + response.string = "0=adrien@grey/" + described_class.stubs(:open).with("http://169.254.169.254/latest/meta-data/public-keys/").returns response + output = described_class.fetch("http://169.254.169.254/latest/meta-data/public-keys/") - Facter::Util::EC2.has_euca_mac?.should == true + expect(output).to eq %w[0/] end - it "should return false when the mac is not a eucalyptus one" do - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("0c:1d:a0:bc:aa:02") + it "returns nil if the endpoint returns a 404" do + described_class.stubs(:open).with("http://169.254.169.254/latest/meta-data/public-keys/1/").raises OpenURI::HTTPError.new("404 Not Found", response) + output = described_class.fetch("http://169.254.169.254/latest/meta-data/public-keys/1/") - Facter::Util::EC2.has_euca_mac?.should == false + expect(output).to be_nil end - end - describe "is_openstack_mac? method" do - it "should return true when the mac is an openstack one" do - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("02:16:3e:54:89:fd") + it "logs an error if the endpoint raises a non-404 HTTPError" do + Facter.expects(:log_exception).with(instance_of(OpenURI::HTTPError), anything) - Facter::Util::EC2.has_openstack_mac?.should == true - end - - it "should return true when the mac is a newer openstack mac" do - # https://github.com/openstack/nova/commit/b684d651f540fc512ced58acd5ae2ef4d55a885c#nova/utils.py - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("fa:16:3e:54:89:fd") + described_class.stubs(:open).with("http://169.254.169.254/latest/meta-data/").raises OpenURI::HTTPError.new("418 I'm a Teapot", response) + output = described_class.fetch("http://169.254.169.254/latest/meta-data/") - Facter::Util::EC2.has_openstack_mac?.should == true + expect(output).to be_nil end - it "should return true when the mac is a newer openstack mac and returned in upper case" do - # https://github.com/openstack/nova/commit/b684d651f540fc512ced58acd5ae2ef4d55a885c#nova/utils.py - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("FA:16:3E:54:89:FD") - - Facter::Util::EC2.has_openstack_mac?.should == true - end + it "logs an error if the endpoint raises a connection error" do + Facter.expects(:log_exception).with(instance_of(Errno::ECONNREFUSED), anything) - it "should return false when the mac is not a openstack one" do - Facter.expects(:value).with(:macaddress).\ - at_least_once.returns("0c:1d:a0:bc:aa:02") + described_class.stubs(:open).with("http://169.254.169.254/latest/meta-data/").raises Errno::ECONNREFUSED + output = described_class.fetch("http://169.254.169.254/latest/meta-data/") - Facter::Util::EC2.has_openstack_mac?.should == false + expect(output).to be_nil end end - describe "can_connect? method" do - it "returns true if api responds" do - # Return something upon connecting to the root - Module.any_instance.expects(:open).with("#{api_prefix}:80/"). - at_least_once.returns("2008-02-01\nlatest") - - Facter::Util::EC2.can_connect?.should be_true + describe "recursively fetching the EC2 metadata API" do + it "queries the given endpoint for metadata keys" do + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/").returns([]) + described_class.recursive_fetch("http://169.254.169.254/latest/meta-data/") end - describe "when connection times out" do - it "should return false" do - # Emulate a timeout when connecting by throwing an exception - Module.any_instance.expects(:open).with("#{api_prefix}:80/"). - at_least_once.raises(RuntimeError) + it "fetches the value for a simple metadata key" do + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/").returns(['indexthing']) + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/indexthing").returns(['first', 'second']) - Facter::Util::EC2.can_connect?.should be_false - end + output = described_class.recursive_fetch("http://169.254.169.254/latest/meta-data/") + expect(output).to eq({'indexthing' => ['first', 'second']}) end - describe "when connection is refused" do - it "should return false" do - # Emulate a connection refused - Module.any_instance.expects(:open).with("#{api_prefix}:80/"). - at_least_once.raises(Errno::ECONNREFUSED) + it "unwraps metadata values that are in single element arrays" do + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/").returns(['ami-id']) + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/ami-id").returns(['i-12x']) - Facter::Util::EC2.can_connect?.should be_false - end + output = described_class.recursive_fetch("http://169.254.169.254/latest/meta-data/") + expect(output).to eq({'ami-id' => 'i-12x'}) end - end - describe "Facter::Util::EC2.userdata" do - let :not_found_error do - OpenURI::HTTPError.new("404 Not Found", StringIO.new) - end + it "recursively queries an endpoint if the key ends with '/'" do + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/").returns(['metrics/']) + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/metrics/").returns(['vhostmd']) + described_class.expects(:fetch).with("http://169.254.169.254/latest/meta-data/metrics/vhostmd").returns(['woo']) - let :example_userdata do - "owner=jeff@puppetlabs.com\ngroup=platform_team" + output = described_class.recursive_fetch("http://169.254.169.254/latest/meta-data/") + expect(output).to eq({'metrics' => {'vhostmd' => 'woo'}}) end + end - it 'returns nil when no userdata is present' do - Facter::Util::EC2.stubs(:read_uri).raises(not_found_error) - Facter::Util::EC2.userdata.should be_nil + describe "determining if a uri is reachable" do + it "retries if the connection times out" do + described_class.stubs(:fetch) + Timeout.expects(:timeout).with(0.2).twice.raises(Timeout::Error).returns(true) + expect(described_class.uri_reachable?("http://169.254.169.254/latest/meta-data/")).to be_true end - it "returns the string containing the body" do - Facter::Util::EC2.stubs(:read_uri).returns(example_userdata) - Facter::Util::EC2.userdata.should == example_userdata + it "retries if the connection is reset" do + described_class.expects(:open).with(anything).twice.raises(Errno::ECONNREFUSED).returns(StringIO.new("woo")) + expect(described_class.uri_reachable?("http://169.254.169.254/latest/meta-data/")).to be_true end - it "uses the specified API version" do - expected_uri = "http://169.254.169.254/2008-02-01/user-data/" - Facter::Util::EC2.expects(:read_uri).with(expected_uri).returns(example_userdata) - Facter::Util::EC2.userdata('2008-02-01').should == example_userdata + it "is false if the given uri returns a 404" do + described_class.expects(:open).with(anything).once.raises(OpenURI::HTTPError.new("404 Not Found", StringIO.new("woo"))) + expect(described_class.uri_reachable?("http://169.254.169.254/latest/meta-data/")).to be_false end end end diff --git a/spec/unit/util/values_spec.rb b/spec/unit/util/values_spec.rb index 557eb195e6..bc347c49a6 100644 --- a/spec/unit/util/values_spec.rb +++ b/spec/unit/util/values_spec.rb @@ -128,4 +128,44 @@ end end end + + describe "flatten_structure" do + it "converts a string to a hash containing that string" do + input = "foo" + output = described_class.flatten_structure("path", input) + expect(output).to eq({"path" => "foo"}) + end + + it "converts an array to a hash with the array elements with indexes" do + input = ["foo"] + output = described_class.flatten_structure("path", input) + expect(output).to eq({"path_0" => "foo"}) + end + + it "prefixes a non-nested hash with the given path" do + input = {"foo" => "bar"} + output = described_class.flatten_structure("path", input) + expect(output).to eq({"path_foo" => "bar"}) + end + + it "flattens elements till it reaches the first non-flattenable structure" do + input = { + "first" => "second", + "arr" => ["zero", "one"], + "nested_array" => [ + "hash" => "string", + ], + "top" => {"middle" => ['bottom']}, + } + output = described_class.flatten_structure("path", input) + + expect(output).to eq({ + "path_first" => "second", + "path_arr_0" => "zero", + "path_arr_1" => "one", + "path_nested_array_0_hash" => "string", + "path_top_middle_0" => "bottom" + }) + end + end end