blob: 8b16fdf59e07e947afc2f01f00208f7ff743bddd [file] [log] [blame]
# Copyright 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Prod host manifest reporting daemon.
This daemon reports metrics about the hosts on production and their
roles to Monarch. This daemon should be run on ONE host, with access
to server_db and with Autotest installed.
"""
from __future__ import print_function
import collections
import subprocess
import sys
import time
from chromite.lib import commandline
from chromite.lib import cros_logging as logging
from chromite.lib import ts_mon_config
from chromite.lib import metrics
DEFAULT_LOG_LEVEL = 'DEBUG'
DEFAULT_INTERVAL = 600 # 10 minutes
METRIC_PREFIX = '/chrome/infra/chromeos/prod_hosts/'
ATEST_PROGRAM = '/usr/local/autotest/cli/atest'
logger = logging.getLogger(__name__)
def main(args):
"""Entry point.
Args:
args: Sequence of command arguments.
"""
opts = ParseArguments(args)
ts_mon_config.SetupTsMonGlobalState('prodmon')
presence_metric = metrics.Boolean(METRIC_PREFIX + 'presence')
roles_metric = metrics.String(METRIC_PREFIX + 'roles')
reporter = ProdHostReporter(
source=AtestSource(atest_program=ATEST_PROGRAM),
sinks=[TsMonSink(presence_metric=presence_metric,
roles_metric=roles_metric),
LoggingSink()])
mainloop = MainLoop(interval=opts.interval,
func=reporter)
mainloop.LoopForever()
def ParseArguments(args):
"""Parse command arguments.
Args:
args: Sequence of command arguments.
"""
parser = commandline.ArgumentParser(
description=__doc__,
default_log_level=DEFAULT_LOG_LEVEL)
parser.add_argument(
'--interval',
default=DEFAULT_INTERVAL,
type=int,
help='time (in seconds) between sampling system metrics')
opts = parser.parse_args(args)
opts.Freeze()
return opts
class MainLoop(object):
"""Main daemon loop.
MainLoop instances have a callable that is run on each loop. The interval
argument determines the number of seconds between each loop.
"""
def __init__(self, interval, func):
"""Initialize instance.
Args:
interval: Seconds between loops.
func: Function to call on each loop.
"""
self._interval = interval
self._func = func
def LoopOnce(self):
"""Run loop once."""
try:
self._func()
except Exception:
logger.exception('Error during metric gathering.')
def LoopForever(self):
"""Run loop forever."""
while True:
self.LoopOnce()
time.sleep(self._interval)
class ProdHostReporter(object):
"""Prod host metrics reporter.
ProdHostReporter takes source and sinks arguments. Sources have a
GetServers() method that returns prod host information as an iterable of
Server instances. Sinks have a WriteServers() method for reporting the
server information.
"""
def __init__(self, source, sinks=()):
"""Initialize instance.
Args:
source: Source for getting prod host information.
sinks: Sinks for writing prod host information.
"""
self._source = source
self._sinks = sinks
def __call__(self):
"""Report prod hosts."""
servers = list(self._source.GetServers())
for sink in self._sinks:
sink.WriteServers(servers)
class _TableParser(object):
"""Parse simple tables.
Each table row is one line of text, with each column in the row separated by
a column separator character. Any whitespace around the separators are
ignored.
"""
def __init__(self, sep):
"""Initialize instance.
Args:
sep: Separator character.
"""
self._sep = sep
def Parse(self, lines):
"""Parse table as output by atest.
Args:
lines: Iterable of table lines as strings.
Returns:
Generator of dicts mapping column names to row values.
"""
rows = (self._SplitTableRow(line) for line in lines)
col_names = next(rows)
for row in rows:
yield dict(zip(col_names, row))
def _SplitTableRow(self, line):
"""Split up columns in one table row.
Args:
line: Line representing table row.
Returns:
List of column values as strings.
"""
return [cell.strip() for cell in line.split(self._sep)]
class _AtestParser(object):
"""Parse `atest server list --table` output."""
_table_parser = _TableParser(sep='|')
def Parse(self, lines):
"""Parse output lines.
Args:
lines: Iterable of lines.
Returns:
Generator of Server instances.
"""
for row in self._table_parser.Parse(lines):
yield self._FormatRow(row)
def _FormatRow(self, row):
"""Format a row into a Server instance.
Args:
row: A table row as a dict.
Returns:
Server instance.
"""
return Server(
hostname=row['Hostname'],
status=row['Status'],
roles=frozenset(row['Roles'].split(',')),
# TODO(ayatane): Parse these into datetimes if/when these are used.
created=row['Date Created'],
modified=row['Date Modified'],
note=row['Note'])
class AtestSource(object):
"""Source for prod host information, using atest."""
_default_parser = _AtestParser()
def __init__(self, atest_program, parser=_default_parser):
"""Initialize instance.
Args:
atest_program: atest program as full path or name on the search path.
parser: Parser to use for atest output.
"""
self._atest_program = atest_program
self._parser = parser
def _QueryAtestForServers(self):
"""Run atest to get host information.
Returns:
atest output as a string (specifically, a bytestring)
"""
return subprocess.check_output(
[self._atest_program, 'server', 'list', '--table'])
def GetServers(self):
"""Get server information from this source.
Returns:
Iterable of Server instances.
"""
output = self._QueryAtestForServers()
output_lines = output.splitlines()
hosts = self._parser.Parse(output_lines)
return hosts
Server = collections.namedtuple('Server', [
'hostname', 'status', 'roles', 'created', 'modified', 'note',
])
class TsMonSink(object):
"""Sink using ts_mon to report Monarch metrics."""
def __init__(self, presence_metric, roles_metric):
"""Initialize instance.
Args:
presence_metric: BooleanMetric instance for reporting host presence.
roles_metric: RolesMetric instance for reporting host roles.
"""
self._presence_metric = presence_metric
self._roles_metric = roles_metric
def WriteServers(self, servers):
"""Write server information.
Args:
servers: Iterable of Server instances.
"""
for server in servers:
fields = {
'target_hostname': server.hostname,
}
self._presence_metric.set(True, fields)
self._roles_metric.set(self._FormatRoles(server.roles), fields)
def _FormatRoles(self, roles):
return ','.join(sorted(roles))
class LoggingSink(object):
"""Sink using Python's logging facilities."""
def WriteServers(self, servers):
"""Write server information.
Args:
servers: Iterable of Server instances.
"""
for server in servers:
logger.debug('Server: %r', server)
if __name__ == '__main__':
main(sys.argv[1:])