From db9c5e48a741f6106a6575d5cb41db3fe0d1fb77 Mon Sep 17 00:00:00 2001 From: Renato Covarrubias Date: Tue, 14 Feb 2023 22:10:34 +0100 Subject: Add cloud provider oci (#1780) ohai cloud fail if the vm is in Oracle Cloud Infrastructure (OCI). This PR add support to this cloud provider to cloud plugin. It also adds OCI detection to the cloud plugin. Signed-off-by: Renato Covarrubias --- .github/workflows/exec.yml | 2 +- lib/ohai/mixin/oci_metadata.rb | 69 ++++++++++++ lib/ohai/plugins/cloud.rb | 22 ++++ lib/ohai/plugins/oci.rb | 94 +++++++++++++++++ spec/unit/mixin/oci_metadata_spec.rb | 66 ++++++++++++ spec/unit/plugins/cloud_spec.rb | 38 +++++++ spec/unit/plugins/oci_spec.rb | 198 +++++++++++++++++++++++++++++++++++ 7 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 lib/ohai/mixin/oci_metadata.rb create mode 100644 lib/ohai/plugins/oci.rb create mode 100644 spec/unit/mixin/oci_metadata_spec.rb create mode 100644 spec/unit/plugins/oci_spec.rb diff --git a/.github/workflows/exec.yml b/.github/workflows/exec.yml index 13dca0ca..ba734d5f 100644 --- a/.github/workflows/exec.yml +++ b/.github/workflows/exec.yml @@ -6,7 +6,7 @@ name: exec push: branches: - main - - 1=7-stable + - 17-stable - 16-stable permissions: diff --git a/lib/ohai/mixin/oci_metadata.rb b/lib/ohai/mixin/oci_metadata.rb new file mode 100644 index 00000000..b04243a3 --- /dev/null +++ b/lib/ohai/mixin/oci_metadata.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# +# Author:: Renato Covarrubias () +# 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" unless defined?(Net::HTTP) + +module Ohai + module Mixin + module OCIMetadata + OCI_METADATA_ADDR = "169.254.169.254" + OCI_METADATA_URL = "/opc/v2" + CHASSIS_ASSET_TAG_FILE = "/sys/devices/virtual/dmi/id/chassis_asset_tag" + + # fetch the meta content with a timeout and the required header + def http_get(uri) + conn = Net::HTTP.start(OCI_METADATA_ADDR) + conn.read_timeout = 6 + conn.get( + uri, + { + "Authorization" => "Bearer Oracle", + "User-Agent" => "chef-ohai/#{Ohai::VERSION}", + } + ) + end + + # parse JSON data from a String to a Hash + # + # @param [String] response_body json as string to parse + # + # @return [Hash] + def parse_json(response_body) + data = String(response_body) + parser = FFI_Yajl::Parser.new + parser.parse(data) + rescue FFI_Yajl::ParseError + logger.warn("Mixin OciMetadata: Metadata response is NOT valid JSON") + nil + end + + # Fetch metadata from api + def fetch_metadata(metadata = "instance") + response = http_get("#{OCI_METADATA_URL}/#{metadata}") + return nil unless response.code == "200" + + if response.code == "200" + parse_json(response.body) + else + logger.warn("Mixin OciMetadata: Received response code #{response.code} requesting metadata") + nil + end + end + end + end +end diff --git a/lib/ohai/plugins/cloud.rb b/lib/ohai/plugins/cloud.rb index 8e79a824..503516ab 100644 --- a/lib/ohai/plugins/cloud.rb +++ b/lib/ohai/plugins/cloud.rb @@ -28,6 +28,7 @@ Ohai.plugin(:Cloud) do depends "azure" depends "digital_ocean" depends "softlayer" + depends "oci" # Class to help enforce the interface exposed to node[:cloud] (OHAI-542) # @@ -336,6 +337,26 @@ Ohai.plugin(:Cloud) do @cloud_attr_obj.provider = "softlayer" end + # ---------------------------------------- + # OCI + # ---------------------------------------- + + # Is current Oracle Cloud Infrastructure? + # + # === Return + # true:: If oci Hash is defined + # false:: Otherwise + def on_oci? + oci != nil + end + + # Fill cloud hash with OCI values + def oci_values + oci["metadata"]["network"]["interface"].each { |vnic| @cloud_attr_obj.add_ipv4_addr(vnic["privateIp"], :private) } + @cloud_attr_obj.local_hostname = oci["metadata"]["compute"]["hostname"] + @cloud_attr_obj.provider = "oci" + end + collect_data do require "ipaddr" unless defined?(IPAddr) @@ -351,6 +372,7 @@ Ohai.plugin(:Cloud) do get_digital_ocean_values if on_digital_ocean? get_softlayer_values if on_softlayer? get_alibaba_values if on_alibaba? + oci_values if on_oci? cloud @cloud_attr_obj.cloud_mash end diff --git a/lib/ohai/plugins/oci.rb b/lib/ohai/plugins/oci.rb new file mode 100644 index 00000000..04e83ba5 --- /dev/null +++ b/lib/ohai/plugins/oci.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# +# Author:: Renato Covarrubias () +# 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. +# + +Ohai.plugin(:Oci) do + require_relative "../mixin/oci_metadata" + require_relative "../mixin/http_helper" + + include Ohai::Mixin::OCIMetadata + include Ohai::Mixin::HttpHelper + + provides "oci" + + collect_data do + oci_metadata_from_hints = hint?("oci") + if oci_metadata_from_hints + logger.trace("Plugin OCI: oci hint is present. Parsing any hint data.") + oci Mash.new + oci_metadata_from_hints.each { |k, v| oci[k] = v } + oci["metadata"] = parse_metadata + elsif oci_chassis_asset_tag? + logger.trace("Plugin oci: No hints present, but system appears to be on oci.") + oci Mash.new + oci["metadata"] = parse_metadata + else + logger.trace("Plugin oci: No hints present and doesn't appear to be on oci.") + false + end + end + + def oci_chassis_asset_tag? + has_oci_chassis_asset_tag = false + if file_exist?(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE) + file_open(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE).each do |line| + next unless /OracleCloud.com/.match?(line) + + logger.trace("Plugin oci: Found OracleCloud.com chassis_asset_tag used by oci.") + has_oci_chassis_asset_tag = true + break + end + end + has_oci_chassis_asset_tag + end + + def parse_metadata + return nil unless can_socket_connect?(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR, 80) + + instance_data = fetch_metadata("instance") + return nil if instance_data.nil? + + metadata = Mash.new + metadata["compute"] = Mash.new + + instance_data.each do |k, v| + metadata["compute"][k] = v + end + + vnics_data = fetch_metadata("vnics") + + unless vnics_data.nil? + metadata["network"] = Mash.new + metadata["network"]["interface"] = [] + vnics_data.each do |v| + metadata["network"]["interface"].append(v) + end + end + + volume_attachments_data = fetch_metadata("volumeAttachments") + + unless volume_attachments_data.nil? + metadata["volumes"] = Mash.new + volume_attachments_data.each do |k, v| + metadata["volumes"][k] = v + end + end + + metadata + end +end diff --git a/spec/unit/mixin/oci_metadata_spec.rb b/spec/unit/mixin/oci_metadata_spec.rb new file mode 100644 index 00000000..3135b028 --- /dev/null +++ b/spec/unit/mixin/oci_metadata_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# +# Author:: Renato Covarrubias +# 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 "spec_helper" +require "ohai/mixin/oci_metadata" + +describe Ohai::Mixin::OCIMetadata do + let(:mixin) do + mixin = Object.new.extend(Ohai::Mixin::OCIMetadata) + mixin + end + + before do + logger = instance_double("Mixlib::Log::Child", trace: nil, debug: nil, warn: nil) + allow(mixin).to receive(:logger).and_return(logger) + end + + describe "#http_get" do + it "gets the passed URI" do + http_mock = double("http") + allow(http_mock).to receive(:read_timeout=) + allow(Net::HTTP).to receive(:start).with(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR).and_return(http_mock) + + expect(http_mock).to receive(:get).with(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR, + { "Authorization" => "Bearer Oracle", + "User-Agent" => "chef-ohai/#{Ohai::VERSION}" }) + mixin.http_get(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR) + end + end + + describe "#fetch_metadata" do + it "returns an empty hash given a non-200 response" do + http_mock = double("http", { code: "404" }) + allow(mixin).to receive(:http_get).and_return(http_mock) + + expect(mixin.logger).not_to receive(:warn) + vals = mixin.fetch_metadata + expect(vals).to eq(nil) + end + + it "returns a populated hash given valid JSON response" do + http_mock = double("http", { code: "200", body: '{ "foo": "bar"}' }) + allow(mixin).to receive(:http_get).and_return(http_mock) + + expect(mixin.logger).not_to receive(:warn) + vals = mixin.fetch_metadata + expect(vals).to eq({ "foo" => "bar" }) + end + end +end diff --git a/spec/unit/plugins/cloud_spec.rb b/spec/unit/plugins/cloud_spec.rb index 95d210e0..82dee74f 100644 --- a/spec/unit/plugins/cloud_spec.rb +++ b/spec/unit/plugins/cloud_spec.rb @@ -87,6 +87,7 @@ describe Ohai::System, "plugin cloud" do @plugin[:gce] = nil @plugin[:digital_ocean] = nil @plugin[:softlayer] = nil + @plugin[:oci] = nil @plugin.run expect(@plugin[:cloud]).to be_nil end @@ -511,4 +512,41 @@ describe Ohai::System, "plugin cloud" do end end + describe "with OCI mash" do + before do + @plugin[:oci] = Mash.new + @plugin[:oci][:metadata] = { + "compute" => { + "hostname" => "my-hostname", + }, + "network" => { + "interface" => [ + { "vnicId" => "ocid1.vnic.oc1.phx.exampleuniqueID", "privateIp" => "10.0.3.6", "vlanTag" => 11, + "macAddr" => "00:00:00:00:00:01", "virtualRouterIp" => "10.0.3.1", "subnetCidrBlock" => "10.0.3.0/24", + "nicIndex" => 0 }, + ], + }, + } + end + + it "doesn't populates cloud vm_name" do + @plugin.run + expect(@plugin[:cloud][:vm_name]).not_to eq("testtest") + end + + it "populates cloud local_hostname" do + @plugin.run + expect(@plugin[:cloud][:local_hostname]).to eq("my-hostname") + end + + it "populates cloud private ip" do + @plugin.run + expect(@plugin[:cloud][:local_ipv4]).to eq(@plugin[:oci][:metadata][:network][:interface][0]["privateIp"]) + end + + it "populates cloud provider" do + @plugin.run + expect(@plugin[:cloud][:provider]).to eq("oci") + end + end end diff --git a/spec/unit/plugins/oci_spec.rb b/spec/unit/plugins/oci_spec.rb new file mode 100644 index 00000000..53737383 --- /dev/null +++ b/spec/unit/plugins/oci_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +# +# Author:: Kaustubh Deorukhkar () +# Copyright:: Copyright (c) Chef Software Inc. +# 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 "spec_helper" +begin + require "win32/registry" unless defined?(Win32::Registry) +rescue LoadError => e + puts "Skipping missing rake dep: #{e}" +end + +describe Ohai::System, "plugin oci" do + let(:plugin) { get_plugin("oci") } + let(:hint) do + { + "local_hostname" => "test-vm", + "provider" => "oci", + } + end + + let(:response_data) do + { + "compute" => { + "availabilityDomain" => "EMIr:PHX-AD-1", + "faultDomain" => "FAULT-DOMAIN-3", + "compartmentId" => "ocid1.tenancy.oc1..exampleuniqueID", + "displayName" => "my-example-instance", + "hostname" => "my-hostname", + "id" => "ocid1.instance.oc1.phx.exampleuniqueID", + "image" => "ocid1.image.oc1.phx.exampleuniqueID", + "metadata" => { + "ssh_authorized_keys" => "example-ssh-key", + }, + "region" => "phx", + "canonicalRegionName" => "us-phoenix-1", + "ociAdName" => "phx-ad-1", + "regionInfo" => { + "realmKey" => "oc1", + "realmDomainComponent" => "oraclecloud.com", + "regionKey" => "PHX", + "regionIdentifier" => "us-phoenix-1", + }, + "shape" => "VM.Standard.E3.Flex", + "state" => "Running", + "timeCreated" => 1_600_381_928_581, + "agentConfig" => { + "monitoringDisabled" => false, + "managementDisabled" => false, + "allPluginsDisabled" => false, + "pluginsConfig" => [ + { "name" => "OS Management Service Agent", "desiredState" => "ENABLED" }, + { "name" => "Custom Logs Monitoring", "desiredState" => "ENABLED" }, + { "name" => "Compute Instance Run Command", "desiredState" => "ENABLED" }, + { "name" => "Compute Instance Monitoring", "desiredState" => "ENABLED" }, + ], + }, + "freeformTags" => { + "Department" => "Finance", + }, + "definedTags" => { + "Operations" => { + "CostCenter" => "42", + }, + }, + }, + "network" => { + "interface" => [ + { "vnicId" => "ocid1.vnic.oc1.phx.exampleuniqueID", "privateIp" => "10.0.3.6", "vlanTag" => 11, + "macAddr" => "00:00:00:00:00:01", "virtualRouterIp" => "10.0.3.1", "subnetCidrBlock" => "10.0.3.0/24", + "nicIndex" => 0 }, + { "vnicId" => "ocid1.vnic.oc1.phx.exampleuniqueID", "privateIp" => "10.0.4.3", "vlanTag" => 12, + "macAddr" => "00:00:00:00:00:02", "virtualRouterIp" => "10.0.4.1", "subnetCidrBlock" => "10.0.4.0/24", + "nicIndex" => 0 }, + ], + }, + } + end + + before do + # skips all the metadata logic unless we want to test it + allow(plugin).to receive(:can_socket_connect?) + .with(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR, 80) + .and_return(false) + end + + shared_examples_for "!oci" do + it "does not set the oci attribute" do + plugin.run + expect(plugin[:oci]).to be_nil + end + end + + shared_examples_for "oci" do + it "sets the oci attribute" do + plugin.run + expect(plugin[:oci]).to be_truthy + expect(plugin[:oci]).to have_key(:metadata) + end + end + + describe "with oci hint file" do + before do + allow(plugin).to receive(:hint?).with("oci").and_return(hint) + end + + it "sets the oci cloud attributes" do + plugin.run + expect(plugin[:oci]["provider"]).to eq("oci") + expect(plugin[:oci]["local_hostname"]).to eq("test-vm") + end + end + + describe "without oci hint file not in OCI" do + before do + allow(plugin).to receive(:hint?).with("oci").and_return(false) + allow(plugin).to receive(:file_exist?).with(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE).and_return(true) + @double_file = double(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE) + allow(@double_file).to receive(:each) + .and_yield("") + allow(plugin).to receive(:file_open).with(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE).and_return(@double_file) + end + + it_behaves_like "!oci" + end + + describe "without oci hint file in OCI" do + before do + allow(plugin).to receive(:hint?).with("oci").and_return(false) + allow(plugin).to receive(:file_exist?).with(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE).and_return(true) + @double_file = double(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE) + allow(@double_file).to receive(:each) + .and_yield("OracleCloud.com") + allow(plugin).to receive(:file_open).with(Ohai::Mixin::OCIMetadata::CHASSIS_ASSET_TAG_FILE).and_return(@double_file) + end + + it_behaves_like "oci" + end + + describe "with non-responsive metadata endpoint" do + before do + allow(plugin).to receive(:hint?).with("oci").and_return({}) + end + + it "does not return metadata information" do + allow(plugin).to receive(:can_socket_connect?) + .with(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR, 80) + .and_return(true) + allow(plugin).to receive(:parse_metadata).and_return(nil) + + plugin.run + expect(plugin[:oci]).to have_key(:metadata) + expect(plugin[:oci][:metadata]).to be_nil + end + end + + describe "with responsive metadata endpoint" do + before do + allow(plugin).to receive(:hint?).with("oci").and_return({}) + allow(plugin).to receive(:can_socket_connect?) + .with(Ohai::Mixin::OCIMetadata::OCI_METADATA_ADDR, 80) + .and_return(true) + allow(plugin).to receive(:parse_metadata).and_return(response_data) + plugin.run + end + + it "returns metadata compute information" do + expect(plugin[:oci][:metadata][:compute][:availabilityDomain]).to eq("EMIr:PHX-AD-1") + expect(plugin[:oci][:metadata][:compute][:compartmentId]).to eq("ocid1.tenancy.oc1..exampleuniqueID") + expect(plugin[:oci][:metadata][:compute][:faultDomain]).to eq("FAULT-DOMAIN-3") + expect(plugin[:oci][:metadata][:compute][:hostname]).to eq("my-hostname") + expect(plugin[:oci][:metadata][:compute][:image]).to eq("ocid1.image.oc1.phx.exampleuniqueID") + expect(plugin[:oci][:metadata][:compute][:region]).to eq("phx") + expect(plugin[:oci][:metadata][:compute][:shape]).to eq("VM.Standard.E3.Flex") + expect(plugin[:oci][:metadata][:compute][:state]).to eq("Running") + end + + it "returns metadata network information" do + expect(plugin[:oci][:metadata][:network][:interface][0][:macAddr]).to eq("00:00:00:00:00:01") + expect(plugin[:oci][:metadata][:network][:interface][0][:privateIp]).to eq("10.0.3.6") + end + end +end -- cgit v1.2.1