[Pkg-puppet-devel] [facter] 42/180: (FACT-185) Implement more modular ec2 query API
Stig Sandbeck Mathisen
ssm at debian.org
Mon Jun 30 15:06:29 UTC 2014
This is an automated email from the git hooks/post-receive script.
ssm pushed a commit to branch master
in repository facter.
commit 5ca456f574147d55e28ddb90a72e7701a3d28200
Author: Adrien Thebo <git at somethingsinistral.net>
Date: Wed Apr 2 16:04:17 2014 -0700
(FACT-185) Implement more modular ec2 query API
The existing EC2 API implemented all methods on a module object, which
made for a rather clumsy API that required intrusive stubbing to test.
This commit implements a new object based API that should be cleaner to
use.
---
lib/facter/ec2/rest.rb | 129 ++++++++++++++++++++++++++
spec/fixtures/unit/ec2/rest/meta-data/root | 20 +++++
spec/unit/ec2/rest_spec.rb | 140 +++++++++++++++++++++++++++++
3 files changed, 289 insertions(+)
diff --git a/lib/facter/ec2/rest.rb b/lib/facter/ec2/rest.rb
new file mode 100644
index 0000000..e9c1fca
--- /dev/null
+++ b/lib/facter/ec2/rest.rb
@@ -0,0 +1,129 @@
+require 'timeout'
+require 'open-uri'
+
+module Facter
+ module EC2
+ CONNECTION_ERRORS = [
+ Errno::EHOSTDOWN,
+ Errno::EHOSTUNREACH,
+ Errno::ENETUNREACH,
+ Errno::ECONNABORTED,
+ Errno::ECONNREFUSED,
+ Errno::ECONNRESET,
+ Errno::ETIMEDOUT,
+ ]
+
+ class Base
+ def reachable?(retry_limit = 3)
+ timeout = 0.2
+ able_to_connect = false
+ attempts = 0
+
+ begin
+ Timeout.timeout(timeout) do
+ open(@baseurl).read
+ end
+ able_to_connect = true
+ rescue OpenURI::HTTPError => e
+ if e.message.match /404 Not Found/i
+ able_to_connect = false
+ else
+ 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
+
+ able_to_connect
+ end
+ end
+
+ class Metadata < Base
+
+ DEFAULT_URI = "http://169.254.169.254/latest/meta-data/"
+
+ def initialize(uri = DEFAULT_URI)
+ @baseurl = uri
+ end
+
+ def fetch(path = '')
+ results = {}
+
+ keys = fetch_endpoint(path)
+ 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] = fetch("#{path}#{key}")
+ else
+ # This is a simple key/value pair, we can just query the given endpoint
+ # and store the results.
+ ret = fetch_endpoint("#{path}#{key}")
+ results[key] = ret.size > 1 ? ret : ret.first
+ end
+ end
+
+ results
+ end
+
+ # @param path [String] The path relative to the object base url
+ #
+ # @return [Array, NilClass]
+ def fetch_endpoint(path)
+ uri = @baseurl + path
+ body = open(uri).read
+ parse_results(body)
+ 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}")
+ return nil
+ end
+
+ private
+
+ def parse_results(body)
+ lines = body.split("\n")
+ lines.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
+ end
+ end
+
+ class Userdata < Base
+ DEFAULT_URI = "http://169.254.169.254/latest/user-data/"
+
+ def initialize(uri = DEFAULT_URI)
+ @baseurl = uri
+ end
+
+ def fetch
+ open(@baseurl).read
+ 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
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/unit/ec2/rest/meta-data/root b/spec/fixtures/unit/ec2/rest/meta-data/root
new file mode 100644
index 0000000..9ec3bbe
--- /dev/null
+++ b/spec/fixtures/unit/ec2/rest/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/ec2/rest_spec.rb b/spec/unit/ec2/rest_spec.rb
new file mode 100644
index 0000000..5c74b49
--- /dev/null
+++ b/spec/unit/ec2/rest_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+require 'facter/ec2/rest'
+
+shared_examples_for "an ec2 rest querier" do
+ describe "determining if the uri is reachable" do
+ it "retries if the connection times out" do
+ subject.stubs(:open).returns(stub(:read => nil))
+ Timeout.expects(:timeout).with(0.2).twice.raises(Timeout::Error).returns(true)
+ expect(subject).to be_reachable
+ end
+
+ it "retries if the connection is reset" do
+ subject.expects(:open).twice.raises(Errno::ECONNREFUSED).returns(StringIO.new("woo"))
+ expect(subject).to be_reachable
+ end
+
+ it "is false if the given uri returns a 404" do
+ subject.expects(:open).with(anything).once.raises(OpenURI::HTTPError.new("404 Not Found", StringIO.new("woo")))
+ expect(subject).to_not be_reachable
+ end
+ end
+
+end
+
+describe Facter::EC2::Metadata do
+
+ subject { described_class.new('http://0.0.0.0/latest/meta-data/') }
+
+ let(:response) { StringIO.new }
+
+ describe "fetching a metadata endpoint" do
+ it "splits the body into an array" do
+ response.string = my_fixture_read("meta-data/root")
+ subject.stubs(:open).with("http://0.0.0.0/latest/meta-data/").returns response
+ output = subject.fetch_endpoint('')
+
+ 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
+
+ it "reformats keys that are array indices" do
+ response.string = "0=adrien at grey/"
+ subject.stubs(:open).with("http://0.0.0.0/latest/meta-data/public-keys/").returns response
+ output = subject.fetch_endpoint("public-keys/")
+
+ expect(output).to eq %w[0/]
+ end
+
+ it "returns nil if the endpoint returns a 404" do
+ Facter.expects(:log_exception).never
+ subject.stubs(:open).with("http://0.0.0.0/latest/meta-data/public-keys/1/").raises OpenURI::HTTPError.new("404 Not Found", response)
+ output = subject.fetch_endpoint('public-keys/1/')
+
+ expect(output).to be_nil
+ end
+
+ it "logs an error if the endpoint raises a non-404 HTTPError" do
+ Facter.expects(:log_exception).with(instance_of(OpenURI::HTTPError), anything)
+
+ subject.stubs(:open).with("http://0.0.0.0/latest/meta-data/").raises OpenURI::HTTPError.new("418 I'm a Teapot", response)
+ output = subject.fetch_endpoint("")
+
+ expect(output).to be_nil
+ end
+
+ it "logs an error if the endpoint raises a connection error" do
+ Facter.expects(:log_exception).with(instance_of(Errno::ECONNREFUSED), anything)
+
+ subject.stubs(:open).with("http://0.0.0.0/latest/meta-data/").raises Errno::ECONNREFUSED
+ output = subject.fetch_endpoint('')
+
+ expect(output).to be_nil
+ end
+ end
+
+ describe "recursively fetching the EC2 metadata API" do
+ it "queries the given endpoint for metadata keys" do
+ subject.expects(:fetch_endpoint).with("").returns([])
+ subject.fetch
+ end
+
+ it "fetches the value for a simple metadata key" do
+ subject.expects(:fetch_endpoint).with("").returns(['indexthing'])
+ subject.expects(:fetch_endpoint).with("indexthing").returns(['first', 'second'])
+
+ output = subject.fetch
+ expect(output).to eq({'indexthing' => ['first', 'second']})
+ end
+
+ it "unwraps metadata values that are in single element arrays" do
+ subject.expects(:fetch_endpoint).with("").returns(['ami-id'])
+ subject.expects(:fetch_endpoint).with("ami-id").returns(['i-12x'])
+
+ output = subject.fetch
+ expect(output).to eq({'ami-id' => 'i-12x'})
+ end
+
+ it "recursively queries an endpoint if the key ends with '/'" do
+ subject.expects(:fetch_endpoint).with("").returns(['metrics/'])
+ subject.expects(:fetch_endpoint).with("metrics/").returns(['vhostmd'])
+ subject.expects(:fetch_endpoint).with("metrics/vhostmd").returns(['woo'])
+
+ output = subject.fetch
+ expect(output).to eq({'metrics' => {'vhostmd' => 'woo'}})
+ end
+ end
+
+ it_behaves_like "an ec2 rest querier"
+end
+
+describe Facter::EC2::Userdata do
+
+ subject { described_class.new('http://0.0.0.0/latest/user-data/') }
+
+ let(:response) { StringIO.new }
+
+ describe "reaching the userdata" do
+ it "queries the userdata URI" do
+ subject.expects(:open).with('http://0.0.0.0/latest/user-data/').returns(response)
+ subject.fetch
+ end
+
+ it "returns the result of the query without modification" do
+ response.string = "clooouuuuud"
+ subject.expects(:open).with('http://0.0.0.0/latest/user-data/').returns(response)
+ expect(subject.fetch).to eq "clooouuuuud"
+ end
+
+ it "is nil if the URI returned a 404" do
+ subject.expects(:open).with('http://0.0.0.0/latest/user-data/').once.raises(OpenURI::HTTPError.new("404 Not Found", StringIO.new("woo")))
+ expect(subject.fetch).to be_nil
+ end
+ end
+
+ it_behaves_like "an ec2 rest querier"
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-puppet/facter.git
More information about the Pkg-puppet-devel
mailing list