diff --git a/lib/ohai/mixin/gce_metadata.rb b/lib/ohai/mixin/gce_metadata.rb new file mode 100644 index 000000000..6a35f5b97 --- /dev/null +++ b/lib/ohai/mixin/gce_metadata.rb @@ -0,0 +1,101 @@ +# +# Author:: Ranjib Dey () +# 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 CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'net/http' +require 'socket' + +module Ohai + module Mixin + module GCEMetadata + + GCE_METADATA_ADDR = "metadata.google.internal" unless defined?(GCE_METADATA_ADDR) + GCE_METADATA_URL = "/0.1/meta-data" unless defined?(GCE_METADATA_URL) + + def can_metadata_connect?(addr, port, timeout=2) + t = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0) + saddr = Socket.pack_sockaddr_in(port, addr) + connected = false + + begin + t.connect_nonblock(saddr) + rescue Errno::EINPROGRESS + r,w,e = IO::select(nil,[t],nil,timeout) + if !w.nil? + connected = true + else + begin + t.connect_nonblock(saddr) + rescue Errno::EISCONN + t.close + connected = true + rescue SystemCallError + end + end + rescue SystemCallError + end + Ohai::Log.debug("can_metadata_connect? == #{connected}") + connected + end + + def http_client + Net::HTTP.start(GCE_METADATA_ADDR).tap {|h| h.read_timeout = 600} + end + + def fetch_metadata(id='') + uri = "#{GCE_METADATA_URL}/#{id}" + response = http_client.get(uri) + return nil unless response.code == "200" + + if json?(response.body) + data = StringIO.new(response.body) + parser = Yajl::Parser.new + parser.parse(data) + elsif has_trailing_slash?(id) or (id == '') + temp={} + response.body.split("\n").each do |sub_attr| + temp[sanitize_key(sub_attr)] = fetch_metadata("#{id}#{sub_attr}") + end + temp + else + response.body + end + end + + def json?(data) + data = StringIO.new(data) + parser = Yajl::Parser.new + begin + parser.parse(data) + true + rescue Yajl::ParseError + false + end + end + + def multiline?(data) + data.lines.to_a.size > 1 + end + + def has_trailing_slash?(data) + !! ( data =~ %r{/$} ) + end + + def sanitize_key(key) + key.gsub(/\-|\//, '_') + end + end + end +end diff --git a/lib/ohai/plugins/cloud.rb b/lib/ohai/plugins/cloud.rb index 0e0ed598d..68852d043 100644 --- a/lib/ohai/plugins/cloud.rb +++ b/lib/ohai/plugins/cloud.rb @@ -17,6 +17,7 @@ provides "cloud" require_plugin "ec2" +require_plugin "gce" require_plugin "rackspace" require_plugin "eucalyptus" require_plugin "linode" @@ -29,6 +30,41 @@ def create_objects cloud[:public_ips] = Array.new cloud[:private_ips] = Array.new end +#--------------------------------------- +# Google Compute Engine (gce) +#-------------------------------------- + +def on_gce? + gce != nil +end +def get_gce_values + cloud[:public_ipv4] = [] + cloud[:local_ipv4] = [] + + public_ips = gce['network']["networkInterface"].collect do |interface| + if interface.has_key?('accessConfiguration') + interface['accessConfiguration'].collect{|ac| ac['externalIp']} + end + end.flatten.compact + + private_ips = gce['network']["networkInterface"].collect do |interface| + interface['ip'] + end.compact + + cloud[:public_ips] += public_ips + cloud[:private_ips] += private_ips + cloud[:public_ipv4] += public_ips + cloud[:public_hostname] = nil + cloud[:local_ipv4] += private_ips + cloud[:local_hostname] = gce['hostname'] + cloud[:provider] = "gce" +end + +# setup gce cloud +if on_gce? + create_objects + get_gce_values +end # ---------------------------------------- # ec2 diff --git a/lib/ohai/plugins/gce.rb b/lib/ohai/plugins/gce.rb new file mode 100644 index 000000000..c93f6d577 --- /dev/null +++ b/lib/ohai/plugins/gce.rb @@ -0,0 +1,40 @@ +# +# Author:: Ranjib Dey () +# 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 CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +provides "gce" + +require 'ohai/mixin/gce_metadata' + +extend Ohai::Mixin::GCEMetadata +GOOGLE_SYSFS_DMI = '/sys/firmware/dmi/entries/1-0/raw' + +#https://developers.google.com/compute/docs/instances#dmi +def has_google_dmi? + ::File.read(GOOGLE_SYSFS_DMI).include?('Google') +end + +def looks_like_gce? + hint?('gce') || has_google_dmi? && can_metadata_connect?(GCE_METADATA_ADDR,80) +end + +if looks_like_gce? + Ohai::Log.debug("looks_like_gce? == true") + gce Mash.new + fetch_metadata.each {|k, v| gce[k] = v } +else + Ohai::Log.debug("looks_like_gce? == false") + false +end diff --git a/spec/unit/plugins/gce_spec.rb b/spec/unit/plugins/gce_spec.rb new file mode 100644 index 000000000..f1908e50a --- /dev/null +++ b/spec/unit/plugins/gce_spec.rb @@ -0,0 +1,130 @@ +# +# Author:: Ranjib Dey (dey.ranjib@gmail.com) +# 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 'open-uri' + +describe Ohai::System, "plugin gce" do + before(:each) do + @ohai = Ohai::System.new + @ohai.stub!(:require_plugin).and_return(true) + end + + shared_examples_for "!gce" do + it "should NOT attempt to fetch the gce metadata" do + @ohai.should_not_receive(:http_client) + @ohai._require_plugin("gce") + end + end + + shared_examples_for "gce" do + before(:each) do + @http_client = mock("Net::HTTP client") + @ohai.stub!(:http_client).and_return(@http_client) + IO.stub!(:select).and_return([[],[1],[]]) + t = mock("connection") + t.stub!(:connect_nonblock).and_raise(Errno::EINPROGRESS) + Socket.stub!(:new).and_return(t) + Socket.stub!(:pack_sockaddr_in).and_return(nil) + end + + it "should recursively fetch metadata" do + @http_client.should_receive(:get). + with("/0.1/meta-data/"). + and_return(mock("Net::HTTPOK", + :body => "domain\nhostname\ndescription", :code=>"200")) + @http_client.should_receive(:get). + with("/0.1/meta-data/domain"). + and_return(mock("Net::HTTPOK", :body => "test-domain", :code=>"200")) + @http_client.should_receive(:get). + with("/0.1/meta-data/hostname"). + and_return(mock("Net::HTTPOK", :body => "test-host", :code=>"200")) + @http_client.should_receive(:get). + with("/0.1/meta-data/description"). + and_return(mock("Net::HTTPOK", :body => "test-description", :code=>"200")) + + @ohai._require_plugin("gce") + + @ohai[:gce].should_not be_nil + @ohai[:gce]['hostname'].should == "test-host" + @ohai[:gce]['domain'].should == "test-domain" + @ohai[:gce]['description'].should == "test-description" + end + + it "should properly parse json metadata" do + @http_client.should_receive(:get). + with("/0.1/meta-data/"). + and_return(mock("Net::HTTP Response", :body => "attached-disks\n", :code=>"200")) + @http_client.should_receive(:get). + with("/0.1/meta-data/attached-disks"). + and_return(mock("Net::HTTP Response", :body => '{"disks":[{"deviceName":"boot", + "index":0,"mode":"READ_WRITE","type":"EPHEMERAL"}]}', :code=>"200")) + + @ohai._require_plugin("gce") + + @ohai[:gce].should_not be_nil + @ohai[:gce]['attached_disks'].should eq({"disks"=>[{"deviceName"=>"boot", + "index"=>0,"mode"=>"READ_WRITE", + "type"=>"EPHEMERAL"}]}) + end + end + + describe "with dmi and metadata address connected" do + it_should_behave_like "gce" + before(:each) do + File.should_receive(:read).with('/sys/firmware/dmi/entries/1-0/raw').and_return('Google') + end + end + + describe "without dmi and metadata address connected" do + it_should_behave_like "!gce" + before(:each) do + File.should_receive(:read).with('/sys/firmware/dmi/entries/1-0/raw').and_return('Test') + end + end + + describe "with hint file" do + it_should_behave_like "gce" + + before(:each) do + File.stub!(:exist?).with('/etc/chef/ohai/hints/gce.json').and_return(true) + File.stub!(:read).with('/etc/chef/ohai/hints/gce.json').and_return('') + File.stub!(:exist?).with('C:\chef\ohai\hints/gce.json').and_return(true) + File.stub!(:read).with('C:\chef\ohai\hints/gce.json').and_return('') + end + end + + describe "without hint file" do + it_should_behave_like "!gce" + + before(:each) do + File.stub!(:exist?).with('/etc/chef/ohai/hints/gce.json').and_return(false) + File.stub!(:exist?).with('C:\chef\ohai\hints/gce.json').and_return(false) + end + end + + describe "with ec2 cloud file" do + it_should_behave_like "!gce" + + 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('') + File.stub!(:exist?).with('C:\chef\ohai\hints/ec2.json').and_return(true) + File.stub!(:read).with('C:\chef\ohai\hints/ec2.json').and_return('') + end + end +end