Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions lib/facter/core/suitable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 33 additions & 25 deletions lib/facter/ec2.rb
Original file line number Diff line number Diff line change
@@ -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
150 changes: 75 additions & 75 deletions lib/facter/util/ec2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<index>=<associated key>/', 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
4 changes: 4 additions & 0 deletions lib/facter/util/resolution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
29 changes: 29 additions & 0 deletions lib/facter/util/values.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

module Facter
module Util
# A util module for facter containing helper methods
Expand Down Expand Up @@ -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
20 changes: 20 additions & 0 deletions spec/fixtures/unit/util/ec2/meta-data/root
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions spec/unit/core/suitable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading