# -*- coding: utf-8 -*-
"""Module for talking to MK-Livestatus sockets."""
from __future__ import absolute_import
import six
import socket
import sys
import time
import pynag.Parsers.errors
import pynag.Parsers.main
import pynag.Utils.paths
# TODO remove this and raise proper exceptions
from pynag.Parsers.errors import ParserError
class Error(ParserError):
"""Base class for errors in this module."""
class LivestatusNotConfiguredException(Error):
""" This exception is raised if we tried to autodiscover path to livestatus and failed """
class LivestatusError(Error):
""" Used when we get errors from Livestatus """
class InvalidResponseFromLivestatus(Error):
"""Used when an unparsable response comes out of livestatus"""
def __init__(self, query, response, *args, **kwargs):
self.query = query
self.response = response
super(InvalidResponseFromLivestatus, self).__init__(*args, **kwargs)
def __str__(self):
message = 'Could not parse response from livestatus.\nQuery:{query}\nResponse: {response}'
return message.format(query=self.query, response=self.response)
class LivestatusQuery(object):
"""Convenience class to help construct a livestatus query.
When talking to Livestatus we use the LQL - the Livestatus Query Language.
Each query contains:
* A Command in the form of 'GET <table>' (e.g. 'GET services')
* Arbritary number of 'Header Lines' in the form of 'Header: Argument'
* An Empty line, i.e. \n
Example Livestatus Queries:
'GET contacts\n'
'GET contacts\nColumns: name alias\n'
Examples on using this class:
>>> query = LivestatusQuery('GET contacts')
>>> query.get_query()
'GET contacts\\n\\n'
>>> query.set_outputformat('python')
>>> query.get_query()
'GET contacts\\nOutputFormat: python\\n\\n'
For more information on Livestatus see:
https://mathias-kettner.de/checkmk_livestatus.html
"""
# The following constants describe names of specific
# Livestatus headers:
_RESPONSE_HEADER = 'ResponseHeader'
_OUTPUT_FORMAT = 'OutputFormat'
_COLUMNS = 'Columns'
_COLUMN_HEADERS = 'ColumnHeaders'
_LIMIT = 'Limit'
_AUTH_USER = 'AuthUser'
_STATS = 'Stats'
_FILTER = 'Filter'
_OR = 'Or'
# How a header line is formatted in a query
_FORMAT_OF_HEADER_LINE = '{keyword}: {arguments}'
# How a typical filter statement looks like:
__FILTER_STATEMENT = 'Filter: {attribute} = {value}'
# This is a mapping from 'pynag style' attribute suffixes into appropriate
# livestatus filter statement.
__FILTER_TRANSMUTATION_SUFFIX = {
'__contains': 'Filter: {attribute} ~~ {value}',
'__has_field': 'Filter: {attribute} >= {value}',
'__isnot': 'Filter: {attribute} != {value}',
'__startswith': 'Filter: {attribute} ~ ^{value}',
'__endswith': 'Filter: {attribute} ~ {value}$',
'__regex': 'Filter: {attribute} ~ {value}',
'__gt': 'Filter: {attribute} > {value}',
'__lt': 'Filter: {attribute} < {value}',
}
def __init__(self, query, *args, **kwargs):
"""Create a new LivestatusQuery.
Args:
query: String. Initial query (like GET hosts).
Technically any object (not only str) having a `splitlines`
method (accepting no arguments) and returning an iterable will be handled as well.
*args: String. Any args will appended to the query as additional headers.
**kwargs: String. Any kwargs will be treated like additional filter to our query.
Examples:
>>> query = LivestatusQuery('GET services')
>>> query.get_query()
'GET services\\n\\n'
>>> query = LivestatusQuery('GET services', 'OutputFormat: json')
>>> query.get_query()
'GET services\\nOutputFormat: json\\n\\n'
>>> query = LivestatusQuery('GET services', 'Columns: service_description', host_name='localhost')
>>> query.get_query()
'GET services\\nColumns: service_description\\nFilter: host_name = localhost\\n\\n'
"""
self._query = []
# We purposefully strip white space, extra line breaks will
# be added to the query string when get_query() is called.
for header_line in query.strip().splitlines():
self.add_header_line(header_line)
for header_line in args:
self.add_header_line(header_line)
self.add_filters(**kwargs)
def __eq__(self, other):
return self._query == other._query
def _join_arguments_with_or(self, *args):
"""join multiple Livestatus Filter statements with a logical OR.
Args:
*args: List of strings like: ['Filter: state = 0', 'Filter: state = 1']
Returns:
List of strings. Same as args except 'Or: %s' % len(args) is added to the list.
Examples:
>>> query = LivestatusQuery('')
>>> query._join_arguments_with_or('', '', '')
['', '', '', 'Or: 3']
>>> query._join_arguments_with_or('')
['']
"""
args = list(args)
length_of_arguments = len(args)
if length_of_arguments > 1:
or_statement = 'Or: %s' % len(args)
args.append(or_statement)
return args
def get_query(self):
"""Get a string representation of our query
Example:
>>> query = LivestatusQuery('GET services')
>>> query.get_query()
'GET services\\n\\n'
>>> query.add_header('Filter', 'host_name = foo')
>>> query.get_query()
'GET services\\nFilter: host_name = foo\\n\\n'
Returns:
A string. String representation of our query that is compatibe
with livestatus.
"""
return '\n'.join(self._query) + '\n\n'
def get_header(self, keyword):
"""Get first header found with keyword in it.
Examples:
>>> query = LivestatusQuery('GET services')
>>> query.get_header('OutputFormat') # Returns None
>>> query.set_outputformat('python')
>>> query.get_header('OutputFormat')
'python'
"""
signature = keyword + ':'
for header in self._query:
if header.startswith(signature):
argument = header.split(':', 1)[1]
argument = argument.strip()
return argument
def column_headers(self):
"""Check if ColumnHeaders are on or off.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.column_headers() # If not set, they are off
False
>>> query.set_columnheaders('on')
>>> query.column_headers()
True
"""
column_headers = self.get_header(self._COLUMN_HEADERS)
if not column_headers or column_headers == 'off':
return False
if column_headers == 'on':
return True
raise LivestatusError("Not sure if ColumnHeaders are on or off, got '%s'" % column_headers)
def output_format(self):
"""Return the currently configured OutputFormat if any is set.
Examples:
>>> query = LivestatusQuery('GET services')
>>> query.output_format() # Returns None
>>> query.set_outputformat('python')
>>> query.output_format()
'python'
"""
return self.get_header(self._OUTPUT_FORMAT)
def add_header_line(self, header_line):
"""Add a new header line to our livestatus query
Example:
>>> query = LivestatusQuery('GET services')
>>> query.add_header_line('Filter: host_name = foo')
>>> query.get_query()
'GET services\\nFilter: host_name = foo\\n\\n'
"""
self._query.append(header_line)
def add_header(self, keyword, arguments):
"""Add a new header to our livestatus query.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.add_header('Filter', 'host_name = foo')
>>> query.get_query()
'GET services\\nFilter: host_name = foo\\n\\n'
"""
header_line = self._FORMAT_OF_HEADER_LINE.format(keyword=keyword, arguments=arguments)
self.add_header_line(header_line)
def has_header(self, keyword):
""" Returns True if specific header is in current query.
Examples:
>>> query = LivestatusQuery('GET services')
>>> query.has_header('OutputFormat')
False
>>> query.add_header('OutputFormat', 'fixed16')
>>> query.has_header('OutputFormat')
True
"""
signature = keyword + ':'
for row in self._query:
if row.startswith(signature):
return True
return False
def remove_header(self, keyword):
"""Remove a header from our query
Examples:
>>> query = LivestatusQuery('GET services')
>>> query.has_header('OutputFormat')
False
>>> query.add_header('OutputFormat', 'fixed16')
>>> query.has_header('OutputFormat')
True
>>> query.remove_header('OutputFormat')
>>> query.has_header('OutputFormat')
False
"""
signature = keyword + ':'
self._query = [x for x in self._query if not x.startswith(signature)]
def set_responseheader(self, response_header='fixed16'):
"""Set ResponseHeader to our query.
Args:
response_header: String. Response header that livestatus knows. Example: fixed16
Example:
>>> query = LivestatusQuery('GET services')
>>> query.set_responseheader()
>>> query.get_query()
'GET services\\nResponseHeader: fixed16\\n\\n'
"""
# First remove whatever responseheader might have been set before
self.remove_header(self._RESPONSE_HEADER)
self.add_header(self._RESPONSE_HEADER, response_header)
def set_outputformat(self, output_format):
"""Set OutFormat header in our query.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.set_outputformat('json')
>>> query.get_query()
'GET services\\nOutputFormat: json\\n\\n'
"""
# Remove outputformat if it was already in out query
self.remove_header(self._OUTPUT_FORMAT)
self.add_header(self._OUTPUT_FORMAT, output_format)
def set_columnheaders(self, status='on'):
"""Turn on or off ColumnHeaders
Example:
>>> query = LivestatusQuery('GET services')
>>> query.set_columnheaders('on')
>>> query.get_query()
'GET services\\nColumnHeaders: on\\n\\n'
>>> query.set_columnheaders('off')
>>> query.get_query()
'GET services\\nColumnHeaders: off\\n\\n'
"""
self.remove_header(self._COLUMN_HEADERS)
self.add_header(self._COLUMN_HEADERS, status)
def set_authuser(self, auth_user):
"""Set AuthUser in our query.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.set_authuser('nagiosadmin')
>>> query.get_query()
'GET services\\nAuthUser: nagiosadmin\\n\\n'
"""
self.remove_header(self._AUTH_USER)
self.add_header(self._AUTH_USER, auth_user)
def has_responseheader(self):
""" Check if there are any ResponseHeaders set.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_responseheader()
False
>>> query.set_responseheader('fixed16')
>>> query.has_responseheader()
True
Returns:
Boolean. True if query has any ResponseHeader, otherwise False.
"""
return self.has_header(self._RESPONSE_HEADER)
def has_authuser(self):
""" Check if AuthUser is set.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_authuser()
False
>>> query.set_authuser('nagiosadmin')
>>> query.has_authuser()
True
Returns:
Boolean. True if query has any AuthUser, otherwise False.
"""
return self.has_header(self._AUTH_USER)
def has_outputformat(self):
""" Check if OutputFormat is set.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_outputformat()
False
>>> query.set_outputformat('python')
>>> query.has_outputformat()
True
Returns:
Boolean. True if query has any OutputFormat set, otherwise False.
"""
return self.has_header(self._OUTPUT_FORMAT)
def has_columnheaders(self):
""" Check if there are any ColumnHeaders set.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_columnheaders()
False
>>> query.set_columnheaders('on')
>>> query.has_columnheaders()
True
Returns:
Boolean. True if query has any ColumnHeaders, otherwise False.
"""
return self.has_header(self._COLUMN_HEADERS)
def has_stats(self):
""" Returns True if Stats headers are present in our query.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_stats()
False
>>> query.add_header('Stats', 'state = 0')
>>> query.has_stats()
True
Returns:
Boolean. True if query has any stats, otherwise False.
"""
return self.has_header(self._STATS)
def has_filters(self):
""" Returns True if any filters are applied.
Example:
>>> query = LivestatusQuery('GET services')
>>> query.has_filters()
False
>>> query.add_header('Filter', 'host_name = localhost')
>>> query.has_filters()
True
Returns:
Boolean. True if query has any filters, otherwise False.
"""
return self.has_header(self._FILTER)
def __str__(self):
"""Wrapper around self.get_query().
Example:
>>> query = LivestatusQuery('GET services', 'Columns: host_name')
>>> str(query)
'GET services\\nColumns: host_name\\n\\n'
"""
return self.get_query()
def splitlines(self, *args, **kwargs):
""" Wrapper around str(self).splitlines().
This function is here for backwards compatibility because a lot of callers were previously passing
in strings, but are now passing in LivestatusQuery. For this purpose we behave like a string.
Example:
>>> query = LivestatusQuery('GET services', 'Columns: host_name')
>>> query.splitlines()
['GET services', 'Columns: host_name', '']
"""
querystring = str(self)
return querystring.splitlines(*args, **kwargs)
def split(self, *args, **kwargs):
""" Wrapper around str(self).split().
This function is here for backwards compatibility because a lot of callers were previously passing
in strings, but are now passing in LivestatusQuery. For this purpose we behave like a string.
Example:
>>> query = LivestatusQuery('GET services', 'Columns: host_name')
>>> query.split('\\n')
['GET services', 'Columns: host_name', '', '']
"""
querystring = str(self)
return querystring.split(*args, **kwargs)
def strip(self, *args, **kwargs):
""" Wrapper around str(self).strip().
This function is here for backwards compatibility because a lot of callers were previously passing
in strings, but are now passing in LivestatusQuery. For this purpose we behave like a string.
Example:
>>> query = LivestatusQuery('GET services')
>>> str(query)
'GET services\\n\\n'
>>> query.strip()
'GET services'
"""
return str(self).strip(*args, **kwargs)
def startswith(self, *args, **kwargs):
""" Wrapper around str(self).startswith().
This function is here for backwards compatibility because a lot of callers were previously passing
in strings, but are now passing in LivestatusQuery. For this purpose we behave like a string.
Example:
>>> query = LivestatusQuery('GET services')
>>> str(query)
'GET services\\n\\n'
>>> query.startswith('GET')
True
"""
return str(self).startswith(*args, **kwargs)
def convert_key_value_to_filter_statement(self, attribute, value):
"""Convert a key/value pair to a Livestatus compatible Filter: statement.
Args:
attribute: String. Name of a single livestatus attribute, for example 'host_name'.
the attribute can have a 'pynag style' suffix which is a hint to what kind of
filtering will be applied. See the examples section for an idea how this is applied.
value: String. Single value to filter by, for example: 'localhost'
Returns:
String. A single livestatus compatible filter statement. See Examples section for
an idea of what return statement looks like.
Examples:
>>> query = LivestatusQuery('')
>>> query.convert_key_value_to_filter_statement('host_name', 'test')
'Filter: host_name = test'
>>> query.convert_key_value_to_filter_statement('service_description__contains', 'serv')
'Filter: service_description ~~ serv'
>>> query.convert_key_value_to_filter_statement('service_description__isnot', 'serv')
'Filter: service_description != serv'
>>> query.convert_key_value_to_filter_statement('service_description__has_field', 'foo')
'Filter: service_description >= foo'
>>> query.convert_key_value_to_filter_statement('service_description__startswith', 'foo')
'Filter: service_description ~ ^foo'
>>> query.convert_key_value_to_filter_statement('service_description__endswith', 'foo')
'Filter: service_description ~ foo$'
>>> query.convert_key_value_to_filter_statement('state__gt', '0')
'Filter: state > 0'
>>> query.convert_key_value_to_filter_statement('state__lt', '1')
'Filter: state < 1'
"""
# Check if attribute ends with any of the suffixes in __FILTER_TRANSMUTATION_SUFFIX
# For example if attribute ends with '__contains' we want the end result
# to be 'Filter: attribute ~ value' (notice the ~ instead of =)
for suffix, potential_filter_statement in self.__FILTER_TRANSMUTATION_SUFFIX.items():
if attribute.endswith(suffix):
suffix_length = len(suffix)
attribute = attribute[:-suffix_length]
filter_statement = potential_filter_statement
break
else:
filter_statement = self.__FILTER_STATEMENT
return filter_statement.format(attribute=attribute, value=value)
def create_filter_statement(self, attribute, values):
"""Create a Livestatus filter statement from a key/value pair.
Args:
attribute: String. Name of a livestatus attribute, example: 'host_name'
values: List of strings. If more than one value is provided the resulting filter
query will be be joined with a logical OR.
Returns:
List of strings. List of Livestatus Filter statements. Look at Examples section
for an idea of what return result looks like.
Examples:
>>> query = LivestatusQuery('')
>>> query.create_filter_statement('host', 'localhost')
['Filter: host = localhost']
>>> query.create_filter_statement('host', ['localhost', 'remote_host'])
['Filter: host = localhost', 'Filter: host = remote_host', 'Or: 2']
"""
return_arguments = []
if not isinstance(values, list):
values = [values]
for value in values:
filter_statement = self.convert_key_value_to_filter_statement(attribute, value)
return_arguments.append(filter_statement)
if len(return_arguments) > 1:
return_arguments = self._join_arguments_with_or(*return_arguments)
return return_arguments
def add_filter(self, attribute, value):
"""Adds a single filter statement to current query.
Args:
attribute. String. Attribute to search for.
value: String. Value to search for.
>>> query = LivestatusQuery('GET services')
>>> query.add_filter('host_name', 'localhost')
>>> query.get_query()
'GET services\\nFilter: host_name = localhost\\n\\n'
"""
filter_statements = self.create_filter_statement(attribute, value)
for statement in filter_statements:
self.add_header_line(statement)
def add_filters(self, **kwargs):
"""Add a new filter statement to current query.
Args:
**kwargs: Every key/value pair is a string. See examples for ideas.
>>> query = LivestatusQuery('GET services')
>>> query.add_filters(host_name='localhost')
>>> query.get_query()
'GET services\\nFilter: host_name = localhost\\n\\n'
>>> query.add_filters(description__contains='Ping')
>>> query.get_query()
'GET services\\nFilter: host_name = localhost\\nFilter: description ~~ Ping\\n\\n'
"""
for key, value in kwargs.items():
self.add_filter(key, value)
def set_columns(self, *columns):
"""Set a Columns header to our query with the specified Columns.
Args:
*columns: List of strings. Examples: ['host_name','description']
Examples:
>>> query = LivestatusQuery('GET hosts')
>>> query.set_columns('name', 'address')
>>> query.get_query()
'GET hosts\\nColumns: name address\\n\\n'
"""
self.remove_header(self._COLUMNS)
self.add_header(self._COLUMNS, ' '.join(columns))
def add_or_statement(self, number):
"""Adds an OR statement to the current set of header lines.
Args:
number: Integer. Tells how many arguments should be joined together.
Examples:
>>> query = LivestatusQuery('GET hosts')
>>> query.add_filters(name='localhost')
>>> query.add_filters(name='otherhost')
>>> query.add_or_statement(2)
>>> query.get_query()
'GET hosts\\nFilter: name = localhost\\nFilter: name = otherhost\\nOr: 2\\n\\n'
"""
self.add_header(self._OR, number)
def set_limit(self, limit):
"""Set a Limit header to our query.
Args:
limit: Limit results to this number (integer)
Examples:
>>> query = LivestatusQuery('GET hosts')
>>> query.set_limit(5)
>>> query.get_query()
'GET hosts\\nLimit: 5\\n\\n'
"""
limit = int(limit)
self.remove_limit()
self.add_header(self._LIMIT, limit)
def remove_limit(self):
"""Remove limit header from this query.
Examples:
>>> query = LivestatusQuery('GET hosts')
>>> query.set_limit(5)
>>> query.get_query()
'GET hosts\\nLimit: 5\\n\\n'
>>> query.remove_limit()
>>> query.get_query()
'GET hosts\\n\\n'
"""
self.remove_header(self._LIMIT)
class Livestatus(object):
""" Class for communicating with Livestatus.
Example usage::
s = Livestatus()
for hostgroup s.get_hostgroups():
print(hostgroup['name'], hostgroup['num_hosts'])
For more information on Livestatus see:
https://mathias-kettner.de/checkmk_livestatus.html
"""
# Its relatively common that livestatus queries fail because they are made
# at the same time as nagios is being reloaded. For that reason we introduce
# one retry on failed queries after waiting for _RETRY_INTERVAL seconds.
_RETRY_INTERVAL = 0.5
def __init__(self, livestatus_socket_path=None, nagios_cfg_file=None, authuser=None):
""" Initilize a new instance of Livestatus
Args:
livestatus_socket_path: Path to livestatus socket (if none specified,
use one specified in nagios.cfg)
nagios_cfg_file: Path to your nagios.cfg. If None then try to
auto-detect
authuser: If specified. Every data pulled is with the access rights
of that contact.
"""
self.nagios_cfg_file = nagios_cfg_file
self.error = None
if not livestatus_socket_path:
main_config = pynag.Parsers.main.MainConfig(nagios_cfg_file)
# Look for a broker_module line in the main config and parse its arguments
# One of the arguments is path to the file socket created
for broker_module in main_config.get_list('broker_module'):
if "livestatus.o" or "livestatus.so" in broker_module:
for arg in broker_module.split()[1:]:
if arg.startswith('/') or '=' not in arg:
livestatus_socket_path = arg
break
else:
# If we get here, then we could not locate a broker_module argument
# that looked like a filename
msg = "No Livestatus socket defined. Make sure livestatus broker module is loaded."
raise ParserError(msg)
self.livestatus_socket_path = livestatus_socket_path
self.authuser = authuser
def test(self, raise_error=True):
""" Test if connection to livestatus socket is working
Args:
raise_error: If set to True, raise exception if test fails,otherwise return False
Raises:
ParserError if raise_error == True and connection fails
Returns:
True -- Connection is OK
False -- there are problems and raise_error==False
"""
try:
self.query("GET hosts")
except Exception:
t, e = sys.exc_info()[:2]
self.error = e
if raise_error:
raise ParserError("got '%s' when testing livestatus socket. error was: '%s'" % (type(e), e))
else:
return False
return True
def _get_socket(self):
""" Returns a socket.socket() instance to communicate with livestatus
Socket might be either unix filesocket or a tcp socket depending in
the content of :py:attr:`livestatus_socket_path`
Returns:
Socket to livestatus instance (socket.socket)
Raises:
:py:class:`LivestatusNotConfiguredException` on failed connection.
:py:class:`ParserError` If could not parse configured TCP address
correctly.
"""
if not self.livestatus_socket_path:
msg = ("We could not find path to MK livestatus socket file."
"Make sure MK livestatus is installed and configured")
raise LivestatusNotConfiguredException(msg)
try:
# If livestatus_socket_path contains a colon, then we assume that
# it is tcp socket instead of a local filesocket
if self.livestatus_socket_path.find(':') > 0:
address, tcp_port = self.livestatus_socket_path.split(':', 1)
if not tcp_port.isdigit():
msg = 'Could not parse host:port "%s". This "%s" does not look like a valid tcp port.'
raise ParserError(msg % (self.livestatus_socket_path, tcp_port))
tcp_port = int(tcp_port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((address, tcp_port))
else:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(self.livestatus_socket_path)
return s
except IOError:
t, e = sys.exc_info()[:2]
msg = "%s while connecting to '%s'. Make sure nagios is running and mk_livestatus loaded."
raise ParserError(msg % (e, self.livestatus_socket_path))
def write(self, livestatus_query):
""" Send a raw livestatus query to livestatus socket.
Queries are corrected and convienient default data are added to the
query before sending it to the socket.
Returns:
A string. The result that comes back from our livestatus socket.
Raises:
LivestatusError if there is a problem writing to socket.
"""
# Lets create a socket and see if we can write to it
livestatus_socket = self._get_socket()
if not six.PY2 and not isinstance(livestatus_query, six.binary_type):
# socket.send() requires binary argument
livestatus_query = livestatus_query.encode()
try:
livestatus_socket.send(livestatus_query)
livestatus_socket.shutdown(socket.SHUT_WR)
filesocket = livestatus_socket.makefile()
result = filesocket.read()
return result
except IOError:
msg = "Could not write to socket '%s'. Make sure you have the right permissions"
raise LivestatusError(msg % self.livestatus_socket_path)
finally:
livestatus_socket.close()
def raw_query(self, query, *args, **kwargs):
""" Perform LQL queries on the livestatus socket.
Args:
query: String. Query to be passed to the livestatus socket
*args: String. Will be appended to query
**kwargs: String. Will be appended as 'Filter:' to query.
For example name='foo' will be appended as 'Filter: name = foo'
In most cases if you already have constructed a livestatus query, you should only
need the query argument, args and kwargs can be used to assist in constructing the query.
For example, the following calls all construct equalant queries:
l = Livestatus()
l.query('GET status\nColumns: requests\n')
l.query('GET status'. 'Columns: requests')
l.query('GET status', Columns:'requests')
Returns:
A string. The results that come out of our livestatus socket.
Raises:
LivestatusError: If there are problems with talking to socket.
"""
livestatus_query = LivestatusQuery(query, *args, **kwargs)
response = self.write(str(livestatus_query))
return response
def _parse_response_header(self, livestatus_response):
if not livestatus_response:
raise LivestatusError("Can't parse empty livestatus response")
rows = livestatus_response.splitlines()
header = rows.pop(0)
data = '\n'.join(rows)
return_code = header.split()[0]
if not return_code.startswith('2'):
error_message = header.strip()
raise LivestatusError("Error '%s' from livestatus: %s" % (return_code, data))
return data
def _process_query(self, livestatus_query):
""" Applies pynag specific quirks and defaults to a Livestatus Query.
The following will be added to our livestatus_query automatically:
* If AuthUser is not specified, we add self.authuser.
* If OutputFormat is not specified, we add python.
* If ResponseHeader is not specified, we add fixed16.
* If ColumnHeaders are not specified, we turn them on.
* If Stats are specified, we turn ColumnHeaders off.
Args:
livestatus_query: LivestatusQuery. The query we will process.
"""
# Implicitly add ResponseHeader if none was specified
if not livestatus_query.has_responseheader():
livestatus_query.set_responseheader('fixed16')
# Implicitly add OutputFormat if none was specified
if not livestatus_query.has_outputformat():
livestatus_query.set_outputformat('python')
# Implicitly turn ColumnHeaders on if none we specified
if not livestatus_query.has_columnheaders():
livestatus_query.set_columnheaders('on')
# Implicitly add AuthUser if one was configured:
if self.authuser and not livestatus_query.has_authuser():
livestatus_query.set_authuser(self.authuser)
# This piece of code is here to workaround a bug in livestatus when
# livestatus_query contains 'Stats' and ColumnHeaders are on.
# * Old behavior: Livestatus turns columnheaders explicitly off.
# * New behavior: Livestatus gives us headers, but corrupted data.
#
# Out of 2 evils we maintain consistency by choosing the older
# behavior of always turning of columnheaders for Stats.
if livestatus_query.has_stats():
livestatus_query.set_columnheaders('off')
def _process_response(self, response_data):
"""Returns livestatus response in a structured format.
Args:
response_data: List of strings. Output from livestatus with PythonFormat On
and without Response headers. Every string in the list represents a single
line of livestatus output.
Returns:
* List of dicts.
* Every element in the list represents one row from livestatus.
* Every dict is a {'str':'str'} where the keys are column names and
the values are column values.
"""
column_headers = response_data.pop(0)
# Lets throw everything into a hashmap before we return
result = []
for line in response_data:
current_row = {}
for i, value in enumerate(line):
column_name = column_headers[i]
current_row[column_name] = value
result.append(current_row)
return result
def query(self, query, *args, **kwargs):
""" Performs LQL queries on the livestatus socket.
Note:
The incoming query is mangled and various default headers
are set on, for more details see _process_query().
Use raw_query() instead if you need more control over the
input and output of Livestatus queries.
Args:
query: String. Query to be passed to the livestatus socket
*args: String. Will be appended to query
**kwargs: Will be appended as 'Filter:' to query.
For example name='foo' will be appended as 'Filter: name = foo'
Returns:
List of dicts. Every item in the list is a row from livestatus and
every row is a dictionary where the keys are column names and values
are columns.
Example return value:
[{'host_name': 'localhost', 'service_description':'Ping'},]
Raises:
LivestatusError: If there is a problem talking to livestatus socket.
"""
# columns parameter exists for backwards compatibility only.
# By default ColumnHeaders are 'on'.
kwargs.pop('columns', None)
livestatus_query = LivestatusQuery(query, *args, **kwargs)
self._process_query(livestatus_query)
# This is we actually send our query into livestatus. livestatus_response is the raw response
# from livestatus socket (string):
try:
livestatus_response = self.write(livestatus_query.get_query())
except LivestatusError:
time.sleep(self._RETRY_INTERVAL)
livestatus_response = self.raw_query(livestatus_query)
if not livestatus_response:
raise InvalidResponseFromLivestatus(query=livestatus_query, response=livestatus_response)
# Parse the response header from livestatus, will raise an exception if
# livestatus returned an error:
response_data = self._parse_response_header(livestatus_response)
if livestatus_query.output_format() != 'python':
return response_data
# Return empty list if we got no results
if not response_data:
return []
try:
response_data = eval(response_data)
except Exception:
raise InvalidResponseFromLivestatus(query=livestatus_query, response=response_data)
# Usually when we query livestatus we get back a 'list of rows',
# however Livestatus had a quirk in the past that if there were Stats
# in the query Instead of returning rows, it would just return a list
# of stats. For backwards Compatibility we cling on to the old bug, of
# not returning 'rows' when asking for stats.
if livestatus_query.has_stats() and len(response_data) == 1:
return response_data[0]
# Backwards compatibility. if ColumnHeaders=Off, we return livestatus
# original format of lists of lists (instead of lists of dicts)
if not livestatus_query.column_headers():
return response_data
return self._process_response(response_data)
def get(self, table, *args, **kwargs):
""" Same as self.query('GET %s' % (table,))
Extra arguments will be appended to the query.
Args:
table: Table from which the data will be retrieved
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Example::
get('contacts', 'Columns: name alias')
Returns:
Answer from livestatus in python format.
"""
return self.query('GET %s' % (table,), *args, **kwargs)
def get_host(self, host_name):
""" Performs a GET query for a particular host
This performs::
'''GET hosts
Filter: host_name = %s''' % host_name
Args:
host_name: name of the host to obtain livestatus data from
Returns:
Answer from livestatus in python format.
"""
return self.query('GET hosts', 'Filter: host_name = %s' % host_name)[0]
def get_service(self, host_name, service_description):
""" Performs a GET query for a particular service
This performs::
'''GET services
Filter: host_name = %s
Filter: service_description = %s''' % (host_name, service_description)
Args:
host_name: name of the host the target service is attached to.
service_description: Description of the service to obtain livestatus
data from.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET services', 'Filter: host_name = %s' % host_name,
'Filter: description = %s' % service_description)[0]
def get_hosts(self, *args, **kwargs):
""" Performs a GET query for all hosts
This performs::
'''GET hosts %s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET hosts', *args, **kwargs)
def get_services(self, *args, **kwargs):
""" Performs a GET query for all services
This performs::
'''GET services
%s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET services', *args, **kwargs)
def get_hostgroups(self, *args, **kwargs):
""" Performs a GET query for all hostgroups
This performs::
'''GET hostgroups
%s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET hostgroups', *args, **kwargs)
def get_servicegroups(self, *args, **kwargs):
""" Performs a GET query for all servicegroups
This performs::
'''GET servicegroups
%s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET servicegroups', *args, **kwargs)
def get_contactgroups(self, *args, **kwargs):
""" Performs a GET query for all contactgroups
This performs::
'''GET contactgroups
%s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET contactgroups', *args, **kwargs)
def get_contacts(self, *args, **kwargs):
""" Performs a GET query for all contacts
This performs::
'''GET contacts
%s %s''' % (*args, **kwargs)
Args:
args, kwargs: These will be appendend to the end of the query to
perform additional instructions.
Returns:
Answer from livestatus in python format.
"""
return self.query('GET contacts', *args, **kwargs)
def get_contact(self, contact_name):
""" Performs a GET query for a particular contact
This performs::
'''GET contacts
Filter: contact_name = %s''' % contact_name
Args:
contact_name: name of the contact to obtain livestatus data from
Returns:
Answer from livestatus in python format.
"""
return self.query('GET contacts', 'Filter: contact_name = %s' % contact_name)[0]
def get_servicegroup(self, name):
""" Performs a GET query for a particular servicegroup
This performs::
'''GET servicegroups
Filter: servicegroup_name = %s''' % servicegroup_name
Args:
servicegroup_name: name of the servicegroup to obtain livestatus data from
Returns:
Answer from livestatus in python format.
"""
return self.query('GET servicegroups', 'Filter: name = %s' % name)[0]
def get_hostgroup(self, name):
""" Performs a GET query for a particular hostgroup
This performs::
'''GET hostgroups
Filter: hostgroup_name = %s''' % hostgroup_name
Args:
hostgroup_name: name of the hostgroup to obtain livestatus data from
Returns:
Answer from livestatus in python format.
"""
return self.query('GET hostgroups', 'Filter: name = %s' % name)[0]
def get_contactgroup(self, name):
""" Performs a GET query for a particular contactgroup
This performs::
'''GET contactgroups
Filter: contactgroup_name = %s''' % contactgroup_name
Args:
contactgroup_name: name of the contactgroup to obtain livestatus data from
Returns:
Answer from livestatus in python format.
"""
return self.query('GET contactgroups', 'Filter: name = %s' % name)[0]