Writing Ohai Custom Plugins¶
Custom Ohai plugins describe additional configuration attributes to be collected by Ohai and provided to the chef-client during chef runs.
Ohai plugins are written in Ruby with a plugin DSL documented below. Being written in Ruby provides access to all of Ruby’s built-in functionality, as well as 3rd party gem functionality. Plugins can parse the output of any local command on the node, or they can fetch data from external APIs. Examples of plugins that users have written: - A plugin to gather node information including datacenter, rack, and rack position from an inventory server - A plugin to gather additional RAID array information from a controller utility - A plugin to gather hardware warranty information from a vendor API
See About Ohai for information on Ohai configuration and usage.
Syntax¶
The syntax for an Ohai plugin is as follows:
Ohai.plugin(:Name) do
provides 'attribute', 'attribute/subattribute'
depends 'attribute', 'attribute'
def shared_method
# some Ruby code that defines the shared method
attribute my_data
end
collect_data(:default) do
# some Ruby code
attribute my_data
end
collect_data(:platform...) do
# some Ruby code that defines platform-specific requirements
attribute my_data
end
end
where
- Required.
(:Name)
is used to identify the plugin; when two plugins have the same(:Name)
, those plugins are joined together and run as if they were a single plugin. This value must be a valid Ruby class name, starting with a capital letter and containing only alphanumeric characters - Required.
provides
is a comma-separated list of one (or more) attributes that are defined by this plugin. This attribute will become an automatic attribute (i.e.node['attribute']
) after it is collected by Ohai at the start of the chef-client run. An attribute can also be defined using anattribute/subattribute
pattern depends
is a comma-separated list of one (or more) attributes that are collected by another plugin; as long as the value is collected by another Ohai plugin, it can be used by any pluginshared_method
defines code that can be shared among one (or more)collect_data
blocks; for example, instead of defining a mash for eachcollect_data
block, the code can be defined as a shared method, and then called from anycollect_data
blockcollect_data
is a block of Ruby code that is called by Ohai when it runs; one (or more)collect_data
blocks can be defined in a plugin, but only a singlecollect_data
block is ever run.collect_data(:default)
is the code block that runs when a node’s platform is not defined by a platform-specificcollect_data
blockcollect_data(:platform)
is a platform-specific code block that is run when a match exists between the node’s platform and thiscollect_data
block; only onecollect_data
block may exist for each platform; possible values::aix
,:darwin
,:freebsd
,:linux
,:openbsd
,:netbsd
,:solaris2
,:windows
, or any other value fromRbConfig::CONFIG['host_os']
my_data
is string (a string value
) or an empty mash ({ :setting_a => 'value_a', :setting_b => 'value_b' }
). This is used to define the data that should be collected by the plugin
For example, the following plugin looks up data on virtual machines hosted in Amazon EC2, Google Compute Engine, Rackspace, Eucalyptus, Linode, OpenStack, and Microsoft Azure:
Ohai.plugin(:Cloud) do
provides 'cloud'
depends 'ec2'
depends 'gce'
depends 'rackspace'
depends 'eucalyptus'
depends 'linode'
depends 'openstack'
depends 'azure'
def create_objects
cloud Mash.new
cloud[:public_ips] = []
cloud[:private_ips] = []
end
...
def on_gce?
gce != nil
end
def get_gce_values
cloud[:public_ipv4] = []
cloud[:local_ipv4] = []
public_ips = gce['instance']['networkInterfaces'].collect do |interface|
if interface.has_key?('accessConfigs')
interface['accessConfigs'].collect{|ac| ac['externalIp']}
end
end.flatten.compact
private_ips = gce['instance']['networkInterfaces'].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['instance']['hostname']
cloud[:provider] = 'gce'
end
...
# with following similar code blocks for each cloud provider
where
provides
defines thecloud
attribute, which is then turned into an object using thecreate_objects
shared method, which then generates a hash based on public or private IP addresses- if the cloud provider is Google Compute Engine, then based on the IP address for the node, the
cloud
attribute data is populated into a hash
To see the rest of the code in this plugin, go to: https://github.com/chef/ohai/blob/master/lib/ohai/plugins/cloud.rb.
Ohai DSL Methods¶
The Ohai DSL is a Ruby DSL that is used to define an Ohai plugin and to ensure that Ohai collects the right data at the start of every chef-client run. The Ohai DSL is a small DSL with a single method that is specific to Ohai plugins. Because the Ohai DSL is a Ruby DSL, anything that can be done using Ruby can also be done when defining an Ohai plugin.
collect_data¶
The collect_data
method is a block of Ruby code that is called by Ohai when it runs. One (or more) collect_data
blocks can be defined in a plugin, but only a single collect_data
block is ever run. The collect_data
block that is run is determined by the platform on which the node is running, which is then matched up against the available collect_data
blocks in the plugin.
- A
collect_data(:default)
block is used when Ohai is not able to match the platform of the node with acollect_data(:platform)
block in the plugin - A
collect_data(:platform)
block is required for each platform that requires non-default behavior
When Ohai runs, if there isn’t a matching collect_data
block for a platform, the collect_data(:default)
block is used. The syntax for the collect_data
method is:
collect_data(:default) do
# some Ruby code
end
or:
collect_data(:platform) do
# some Ruby code
end
where:
:default
is the name of the defaultcollect_data
block:platform
is the name of a platform, such as:aix
for AIX or:windows
for Microsoft Windows
Use a Mash¶
Use a mash to store data. This is done by creating a new mash, and then setting an attribute to it. For example:
provides 'name_of_mash'
name_of_mash Mash.new
name_of_mash[:attribute] = 'value'
Examples¶
The following examples show how to use the collect_data
block:
Ohai.plugin(:Azure) do
provides 'azure'
collect_data do
azure_metadata_from_hints = hint?('azure')
if azure_metadata_from_hints
Ohai::Log.debug('azure_metadata_from_hints is present.')
azure Mash.new
azure_metadata_from_hints.each {|k, v| azure[k] = v }
else
Ohai::Log.debug('No hints present for azure.')
false
end
end
end
or:
require 'ohai/mixin/ec2_metadata'
extend Ohai::Mixin::Ec2Metadata
Ohai.plugin do
provides 'openstack'
collect_data do
if hint?('openstack') || hint?('hp')
Ohai::Log.debug('ohai openstack')
openstack Mash.new
if can_metadata_connect?(EC2_METADATA_ADDR,80)
Ohai::Log.debug('connecting to the OpenStack metadata service')
self.fetch_metadata.each {|k, v| openstack[k] = v }
case
when hint?('hp')
openstack['provider'] = 'hp'
else
openstack['provider'] = 'openstack'
end
else
Ohai::Log.debug('unable to connect to the OpenStack metadata service')
end
else
Ohai::Log.debug('NOT ohai openstack')
end
end
end
require¶
The require
method is a standard Ruby method that can be used to list files that may be required by a platform, such as an external class library. As a best practice, even though the require
method is often used at the top of a Ruby file, it is recommended that the use of the require
method be used as part of the platform-specific collect_data
block. For example, the Ruby WMI is required with Microsoft Windows:
collect_data(:windows) do
require 'ruby-wmi'
WIN32OLE.codepage = WIN32OLE::CP_UTF8
kernel Mash.new
host = WMI::Win32_OperatingSystem.find(:first)
kernel[:os_info] = Mash.new
host.properties_.each do |p|
kernel[:os_info][p.name.wmi_underscore.to_sym] = host.send(p.name)
end
...
end
Ohai will attempt to fully qualify the name of any class by prepending Ohai::
to the loaded class. For example both:
require Ohai::Mixin::ShellOut
and:
require Mixin::ShellOut
are both understood by the Ohai in the same way: Ohai::Mixin::ShellOut
.
When a class is an external class (and therefore should not have Ohai::
prepended), use ::
to let the Ohai know. For example:
::External::Class::Library
/common Directory¶
The /common
directory stores code that is used across all Ohai plugins. For example, a file in the /common
directory named virtualization.rb
that includes code like the following:
module Ohai
module Common
module Virtualization
def host?(virtualization)
!virtualization.nil? && virtualization[:role].eql?('host')
end
def open_virtconn(system)
begin
require 'libvirt'
require 'hpricot'
rescue LoadError => e
Ohai::Log.debug('Cannot load gem: #{e}.')
end
emu = (system.eql?('kvm') ? 'qemu' : system)
virtconn = Libvirt::open_read_only('#{emu}:///system')
end
...
def networks(virtconn)
networks = Mash.new
virtconn.list_networks.each do |n|
nv = virtconn.lookup_network_by_name n
networks[n] = Mash.new
networks[n][:xml_desc] = (nv.xml_desc.split('\n').collect {|line| line.strip}).join
['bridge_name','uuid'].each {|a| networks[n][a] = nv.send(a)}
#xdoc = Hpricot networks[n][:xml_desc]
end
networks
end
...
end
end
end
can then be leveraged in a plugin by using the require
method to require the virtualization.rb
file and then later calling each of the methods in the required module:
require 'ohai/common/virtualization'
Ohai.plugin(:Virtualization) do
include Ohai::Common::Virtualization
provides 'virtualization'
%w{ capabilities domains networks storage }.each do |subattr|
provides 'virtualization/#{subattr}'
end
collect_data(:linux) do
virtualization Mash.new
...
if host?(virtualization)
v = open_virtconn(virtualization[:system])
virtualization[:libvirt_version] = libvirt_version(v)
virtualization[:nodeinfo] = nodeinfo(v)
virtualization[:uri] = uri(v)
virtualization[:capabilities] = capabilities(v)
virtualization[:domains] = domains(v)
virtualization[:networks] = networks(v)
virtualization[:storage] = storage(v)
close_virtconn(v)
end
Logging¶
Use the Ohai::Log
class in an Ohai plugin to define log entries that are created by Ohai. The syntax for a log message is as follows:
Ohai::Log.log_type('message')
where
log_type
can be.debug
,.info
,.warn
,.error
, or.fatal
'message'
is the message that is logged.
For example:
Ohai.plugin do
provides 'openstack'
collect_data do
if hint?('openstack') || hint?('hp')
Ohai::Log.debug('ohai openstack')
openstack Mash.new
if can_metadata_connect?(EC2_METADATA_ADDR,80)
Ohai::Log.debug('connecting to the OpenStack metadata service')
self.fetch_metadata.each {|k, v| openstack[k] = v }
case
when hint?('hp')
openstack['provider'] = 'hp'
else
openstack['provider'] = 'openstack'
end
else
Ohai::Log.debug('unable to connect to the OpenStack metadata service')
end
else
Ohai::Log.debug('NOT ohai openstack')
end
end
end
rescue¶
Use the rescue
clause to make sure that a log message is always provided. For example:
rescue LoadError => e
Ohai::Log.debug('ip_scopes: cannot load gem, plugin disabled: #{e}')
end
Examples¶
Note
See https://github.com/rackerlabs/ohai-plugins/tree/master/plugins for some great examples of custom Ohai plugins.
The following examples show different ways of building Ohai plugins.
collect_data Blocks¶
The following Ohai plugin uses multiple collect_data
blocks and shared methods to define platforms:
Ohai.plugin(:Hostname) do
provides 'domain', 'fqdn', 'hostname'
def from_cmd(cmd)
so = shell_out(cmd)
so.stdout.split($/)[0]
end
def collect_domain
if fqdn
fqdn =~ /.+?\.(.*)/
domain $1
end
end
collect_data(:aix, :hpux) do
hostname from_cmd('hostname -s')
fqdn from_cmd('hostname')
domain collect_domain
end
collect_data(:darwin, :netbsd, :openbsd) do
hostname from_cmd('hostname -s')
fqdn from_cmd('hostname')
domain collect_domain
end
collect_data(:freebsd) do
hostname from_cmd('hostname -s')
fqdn from_cmd('hostname -f')
domain collect_domain
end
collect_data(:linux) do
hostname from_cmd('hostname -s')
begin
fqdn from_cmd('hostname --fqdn')
rescue
Ohai::Log.debug('hostname -f returned an error, probably no domain is set')
end
domain collect_domain
end
collect_data(:solaris2) do
require 'socket'
hostname from_cmd('hostname')
fqdn_lookup = Socket.getaddrinfo(hostname, nil, nil, nil, nil, Socket::AI_CANONNAME).first[2]
if fqdn_lookup.split('.').length > 1
# we received an fqdn
fqdn fqdn_lookup
else
# default to assembling one
h = from_cmd('hostname')
d = from_cmd('domainname')
fqdn '#{h}.#{d}'
end
domain collect_domain
end
collect_data(:windows) do
require 'ruby-wmi'
require 'socket'
host = WMI::Win32_ComputerSystem.find(:first)
hostname '#{host.Name}'
info = Socket.gethostbyname(Socket.gethostname)
if info.first =~ /.+?\.(.*)/
fqdn info.first
else
# host is not in dns. optionally use:
# C:\WINDOWS\system32\drivers\etc\hosts
fqdn Socket.gethostbyaddr(info.last).first
end
domain collect_domain
end
end
Use a mixin Library¶
The following Ohai example shows a plugin can use a mixin
library and also depend on another plugin:
require 'ohai/mixin/os'
Ohai.plugin(:Os) do
provides 'os', 'os_version'
depends 'kernel'
collect_data do
os collect_os
os_version kernel[:release]
end
end
Get Kernel Values¶
The following Ohai example shows part of a file that gets initial kernel attribute values:
Ohai.plugin(:Kernel) do
provides 'kernel', 'kernel/modules'
def init_kernel
kernel Mash.new
[['uname -s', :name], ['uname -r', :release],
['uname -v', :version], ['uname -m', :machine]].each do |cmd, property|
so = shell_out(cmd)
kernel[property] = so.stdout.split($/)[0]
end
kernel
end
...
collect_data(:darwin) do
kernel init_kernel
kernel[:os] = kernel[:name]
so = shell_out('sysctl -n hw.optional.x86_64')
if so.stdout.split($/)[0].to_i == 1
kernel[:machine] = 'x86_64'
end
modules = Mash.new
so = shell_out('kextstat -k -l')
so.stdout.lines do |line|
if line =~ /(\d+)\s+(\d+)\s+0x[0-9a-f]+\s+0x([0-9a-f]+)\s+0x[0-9a-f]+\s+([a-zA-Z0-9\.]+) \(([0-9\.]+)\)/
kext[$4] = { :version => $5, :size => $3.hex, :index => $1, :refcount => $2 }
end
end
kernel[:modules] = modules
end
...
Migrating Ohai 6 Plugins¶
Ohai 7 (Chef 11.12) introduced a new and more robust plugin DSL. In Ohai/Chef 14, support for loading existing Ohai V6 plugins will be removed. It is recommended that all Ohai 6 plugins be updated for new DSL behavior in Ohai 7 as soon as possible. When migrating Ohai 6 plugins to Ohai 7, consider the following:
- Pick a name for the existing plugin, and then define it as an Ohai 7 plugin
- Convert the
required_plugin()
calls todepends
statements - Move the Ohai 6 plugin logic into a
collect_data()
block
For example, Ohai 6:
provides 'my_app'
require_plugin('kernel')
my_app Mash.new
my_app[:version] = shell_out('my_app -v').stdout
my_app[:message] = 'Using #{kernel[:version]}'
and then Ohai 7:
Ohai.plugin(:MyAPP) do
provides 'my_app'
depends 'kernel'
collect_data do
my_app Mash.new
my_app[:version] = shell_out('my_app -v').stdout
my_app[:message] = 'Using #{kernel[:version]}'
end
end
Another example, for Ohai 6:
provide 'ipaddress'
require_plugin '#{os}::network'
require_plugin '#{os}::virtualization'
require_plugin 'passwd'
if virtualization['system'] == 'vbox'
if etc['passwd'].any? { |k,v| k == 'vagrant'}
if network['interfaces']['eth1']
network['interfaces']['eth1']['addresses'].each do |ip, params|
if params['family'] == ('inet')
ipaddress ip
end
end
end
end
end
and then Ohai 7:
Ohai.plugin(:Vboxipaddress) do
provides 'ipaddress'
depends 'ipaddress', 'network/interfaces', 'virtualization/system', 'etc/passwd'
collect_data(:default) do
if virtualization['system'] == 'vbox'
if etc['passwd'].any? { |k,v| k == 'vagrant'}
if network['interfaces']['eth1']
network['interfaces']['eth1']['addresses'].each do |ip, params|
if params['family'] == ('inet')
ipaddress ip
end
end
end
end
end
end
end