[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