DigitalOcean, Chef and Ohai – Retrieving a Droplet’s Private IP Address

Recently, I attempted to use the Ohai value for node['cloud_v2']['local_ipv4'] and node['cloud']['local_ipv4']['ip_address'] to determine the Private IP address of my Cloud-based nodes in a Chef cookbook.  Unfortunately, it does not work accurately for DigitalOcean instances any longer.

According to DigitalOcean documentation, if Private Networking is enabled, the IP will be assigned to eth1.  Recently, I noticed that a second Private IP address has begun to be assigned to the eth0 interface.  This is/was causing Ohai to assign the eth0 secondary (private) IP address to node['cloud_v2']['local_ipv4'] and node['cloud']['local_ipv4']['ip_address']

As you can see below, there is a second, private IP address assigned to eth0. I believe this has something to do with the recent release of Floating IP Addresses.

bdwyertech@dummy-droplet:~$ cat /etc/network/interfaces
# This file describes the network interfaces available on your
# interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth1 eth0
      iface eth0 inet static
      up ip addr add dev eth0
iface eth1 inet static

Initially, I just wrote a simple function to detect the IP address on eth1 via the node hash if DigitalOcean is detected by Ohai as the cloud provider. However, querying DigitalOcean metadata seems to be the more robust solution.

DigitalOcean has released a metadata service, similar to AWS, where you can query{API_VERSION} for droplet information.  DigitalOcean conveniently allows you to query the droplet’s metadata in its entirety and return it in JSON format.  I’ve went ahead and written a simple library to query this data and bring it into Ruby as a hash.

# DigitalOcean Metadata Chef Library
# rubocop:disable LineLength

require 'net/http'

# Public: This defines a module to retrieve Metadata from DigitalOcean
module DoMetadata
  DO_METADATA_ADDR = '' unless defined?(DO_METADATA_ADDR)

  def self.http_client
    Net::HTTP.start(DO_METADATA_ADDR).tap { |h| h.read_timeout = 600 }

  # Get metadata for a given path and API version
  def metadata_get(id, api_version = DO_DEFAULT_API_VERSION, json = false)
    path = "/metadata/#{api_version}/#{id}"
    path = "/metadata/#{api_version}.json" if json
    response = http_client.get(path)
    case response.code
    when '200'
    when '404'"Encountered 404 response retreiving DO metadata path: #{path} ; continuing.")
      fail "Encountered error retrieving DO metadata (#{path} returned #{response.code} response)"
  module_function :metadata_get

  # Retrieve the JSON metadata, and return it as a Ruby hash
  def parse_json_metadata(api_version = DO_DEFAULT_API_VERSION)
    retrieved_metadata = metadata_get(nil, api_version, true)
    return unless retrieved_metadata
    JSON.parse(retrieved_metadata) if retrieved_metadata
  module_function :parse_json_metadata

Code also available on my Github.

This conveniently allows you to query the resulting Ruby hash, and use it in your code.

# => Get the Droplet's Metadata
metadata = DoMetadata.parse_json_metadata

metadata['interfaces']['private'][0]['ipv4']['ip_address'] # => Droplet's Private IP Address
metadata['interfaces']['private'][0]['ipv4']['netmask'] # => Droplet's Private Subnet Mask