Skip to content

Repository files navigation

Icinga2 API Client

GitHub license CI Code Climate Test Coverage

A Ruby gem to interact easily with the Icinga2 API.

It exposes a small, chainable object model on top of the REST API:

client.hosts.find('web01').services.find('ssh').schedule_downtime(...)

Largely copied (and adapted) from gdi/ruby-nagios-api-client.

Table of contents

Requirements

  • Ruby >= 3.2 (also tested on JRuby and TruffleRuby)
  • An Icinga2 instance with the API feature enabled and an API user

Install

Add it to your Gemfile:

gem 'icinga2-api-client'

Then run bundle install, or install it directly:

gem install icinga2-api-client

Getting started

require 'icinga2/api'

client = Icinga2::API::Client.new('https://icinga.example.net:5665',
  version:  'v1',
  username: 'admin',
  password: 'pass'
)

client.hosts.all              # => [#<Icinga2::API::Host>, ...]
client.hosts.find('web01')    # => #<Icinga2::API::Host> (or nil if not found)

Client.new takes the base URL of the API and an options hash. The options are passed through to the underlying connection (see Configuration).

Every object exposes its raw Icinga2 attributes through method calls and as a hash:

host = client.hosts.find('web01')
host.name          # => "web01"
host.state         # => 1.0
host.to_h          # => { name: "web01", state: 1.0, ... }
host.to_h(only: [:name, :state])
host.to_h(except: [:vars])

Configuration

Options passed to Icinga2::API::Client.new (second argument):

Option Default Description
username API user (required, HTTP basic auth)
password API user password (required)
version 'v1' API version segment used in request paths
ssl_options {} Hash forwarded to Faraday's :ssl option
open_timeout nil Connection open timeout in seconds (no timeout by default)
timeout 30 Request read timeout in seconds
logging {} Request logging, see below

SSL / TLS

ssl_options is forwarded verbatim to Faraday's ssl option. To talk to an instance with a self-signed certificate, disable verification:

client = Icinga2::API::Client.new('https://icinga.example.net:5665',
  username:    'admin',
  password:    'pass',
  ssl_options: { verify: false }
)

To pin a CA bundle instead:

ssl_options: { ca_file: '/etc/icinga2/ca.crt' }

Timeouts

The read timeout defaults to 30 seconds; open_timeout is unset. Override both (in seconds) to suit your environment:

client = Icinga2::API::Client.new('https://icinga.example.net:5665',
  username:     'admin',
  password:     'pass',
  open_timeout: 5,
  timeout:      30
)

Logging

Pass a logger to trace HTTP requests/responses (Faraday's logger middleware):

require 'logger'

client = Icinga2::API::Client.new('https://icinga.example.net:5665',
  username: 'admin',
  password: 'pass',
  logging: {
    enabled: true,
    logger:  Logger.new($stdout),
    options: { headers: true, bodies: true }
  }
)

Usage

The examples below assume a configured client and require 'yaml' for the YAML.dump calls used to pretty-print objects.

Hosts

# All hosts (Array of Icinga2::API::Host)
puts YAML.dump client.hosts.all.map(&:to_s)

# A single host by name (Icinga2::API::Host, or nil)
puts YAML.dump client.hosts.find('web01').to_s

Services

# All services of a host (Array of Icinga2::API::Service)
puts YAML.dump client.hosts.find('web01').services.all.map(&:to_s)

# A single service by name
puts YAML.dump client.hosts.find('web01').services.find('ssh').to_s

Downtimes

Schedule a downtime

duration   = 3600 # seconds
start_time = Time.now
end_time   = start_time + duration

# Icinga expects Unix timestamps, so call #to_i on the times.
# The `duration` param is mandatory even when `fixed` is true (the default).
# Returns the created Icinga2::API::Downtime (raises ArgumentError if a
# required parameter is missing).
downtime =
  client.hosts
        .find('web01')
        .services
        .find('ssh')
        .schedule_downtime(
          author:     'admin',
          comment:    'Maintenance window',
          start_time: start_time.to_i,
          end_time:   end_time.to_i,
          duration:   duration.to_i
        )

# A whole host can be put in downtime too (same parameters):
client.hosts.find('web01').schedule_downtime(
  author: 'admin', comment: 'Reboot', start_time: start_time.to_i,
  end_time: end_time.to_i, duration: duration.to_i
)

List downtimes

# All downtimes of every service on a host
client.hosts.find('web01').services.downtimes

# Downtimes of a single service
client.hosts.find('web01').services.find('ssh').downtimes

# Host-level downtimes
client.hosts.find('web01').downtimes

Acknowledge a problem

# author and comment are required
client.hosts.find('web01').services.find('ssh').acknowledge(
  author: 'admin', comment: 'Looking into it'
)

# Hosts can be acknowledged too
client.hosts.find('web01').acknowledge(author: 'admin', comment: 'On it')

# Remove an acknowledgement
client.hosts.find('web01').services.find('ssh').remove_acknowledgement

Comments

service = client.hosts.find('web01').services.find('ssh')

comment = service.add_comment(author: 'admin', comment: 'Investigating')
service.comments            # => [Icinga2::API::Comment, ...]
comment.remove

# Host-level comments work the same way
client.hosts.find('web01').comments

Cancel a downtime

client.hosts
      .find('web01')
      .services
      .find('http')
      .downtimes
      .first
      .cancel

Notifications

A Notification is the configuration deciding who gets told about a problem: it is not Actions#send_notification, which fires a one-off custom notification.

# Notification rules attached to the host itself
client.hosts.find('web01').notifications

# ... and to one of its services
client.hosts.find('web01').services.find('ssh').notifications

# Every notification on the server. Mind the volume: a mid-sized master carries
# tens of thousands of them, so filter and restrict the attributes returned.
client.notifications.all(
  filter: 'notification.host_name=="web01"',
  attrs:  %w[__name users period]
)

From a notification you can reach what it points at. The readers named after an attribute keep returning what the API sent (#users is an Array of names); resolving them into objects is done through the explicitly named methods:

notification = client.hosts.find('web01').notifications.first

notification.users                 # => ["admin", "oncall"]   (raw, no request)
notification.notified_users        # => [#<User admin>, ...]  (one request)
notification.notified_user_groups
notification.time_period           # => #<TimePeriod 24x7>
notification.notification_command
notification.host                  # => #<Host web01>
notification.service               # => nil for a host-level notification

Groups, users and time periods

client.users.all
client.user_groups.all
client.time_periods.all
client.host_groups.all
client.service_groups.all
client.dependencies.all
client.scheduled_downtimes.all

Membership is held by the member, not by the group, so both directions are available:

host = client.hosts.find('web01')
host.groups        # => ["linux", "prod"]   (raw names, no request)
host.host_groups   # => [#<HostGroup linux>, #<HostGroup prod>]

client.host_groups.find('linux').hosts   # => the members
client.user_groups.find('oncall').users

ScheduledDowntime is a recurring rule, not an active downtime — the downtimes it creates are what Host#downtimes and Service#downtimes return:

host.scheduled_downtimes   # the rules
host.downtimes             # the downtimes currently in effect

Generic objects

Every collection above is a Icinga2::API::Objects over a type described by the catalog shipped with the gem (data/icinga2_types.json, a frozen snapshot of the server's own /v1/types). Any catalogued type is reachable generically:

client.objects(:time_period).all(filter: 'timeperiod.__name=="24x7"')
client.objects(:notification).find('web01!ssh!mail')

#all accepts filter:, attrs: and joins:, and switches by itself to POST with X-HTTP-Method-Override: GET when the query outgrows the request line. References declared by Icinga2 can be followed generically too:

notification.navigate(:period)   # => #<TimePeriod 24x7>
notification.navigate(:users)    # => [#<User admin>, ...]  (a single request)

#navigate deliberately does not define readers named after the fields it follows. notification.period keeps returning the String the API sent, so an attribute never changes type under an existing caller.

The catalog covers the types this gem models: Host, Service, Downtime, Comment, Notification, User, UserGroup, TimePeriod, HostGroup, ServiceGroup, Dependency, ScheduledDowntime. Anything else raises Icinga2::API::Error::UnknownType, locally, without a request being sent.

Event stream

Client#subscribe opens the Icinga2 event stream and blocks, yielding each event as a Hash until the connection is closed (break out of the block to stop):

client.subscribe(types: ['CheckResult', 'StateChange'], queue: 'my-queue') do |event|
  puts event['type']
end

Error handling

Transport errors are wrapped in this gem's own hierarchy so you never have to rescue Faraday exceptions directly. All of them inherit from Icinga2::API::Error:

Exception Raised when
Icinga2::API::Error::NotFound server returned 404
Icinga2::API::Error::ClientError server returned another 4xx
Icinga2::API::Error::ServerError server returned a 5xx
Icinga2::API::Error::Timeout the request timed out
Icinga2::API::Error::ConnectionFailed the connection could not be opened
Icinga2::API::Error::UnknownType the type is not in the catalog
Icinga2::API::Error::UnknownRelation the field declares no reference

The last two are raised locally, before anything is sent: they signal a mistake in the call, not a server failure. They are siblings, so rescuing one does not swallow the other.

begin
  client.hosts.find('web01').services.find('ssh').schedule_downtime(...)
rescue Icinga2::API::Error => e
  warn "Icinga2 request failed: #{e.message}"
end

Client#hosts.find swallows NotFound and returns nil; every other error propagates.

Thread safety

A Client (and its underlying connection) is not safe to share across threads or fibers concurrently. Use one Client per thread/fiber, or guard access with your own mutex.

Development

bundle install     # install dependencies
bin/rspec          # run the test suite (uses VCR cassettes, no live server needed)
bin/rubocop        # lint
bin/guard          # watch files and re-run specs on change

Read-only integration specs can be run against a real Icinga2 server (they are excluded from the default run):

ICINGA_INTEGRATION=1 \
ICINGA_API_URL=https://icinga.example.net:5665 \
ICINGA_API_USER=root ICINGA_API_PASSWORD=secret \
bin/rspec spec/integration

Regenerating the type catalog

data/icinga2_types.json is a frozen snapshot of a master's /v1/types. It is committed rather than fetched at runtime so that the gem's surface does not depend on which server is contacted and the suite runs without one. Regenerate it after an Icinga2 upgrade, and review the diff:

ICINGA_API_URL=https://icinga.example.net:5665 \
ICINGA_API_PASSWORD=secret \
bin/rake types:snapshot

# ... or from a saved /v1/types response, without handing over credentials:
TYPES_JSON=./types.json bin/rake types:snapshot

The output is deterministic: the same input yields a byte-identical file.

Contribute

You can contribute to this gem in many ways, such as:

  • Helping with documentation
  • Contributing code (features or bugfixes)
  • Reporting a bug
  • Submitting translations

A big thank you to all the contributors, and to gdi/ruby-nagios-api-client for the inspiration.

License

Released under the MIT License.

About

A Ruby gem to interact easily with the Icinga2 API ;)

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages