pythondir = $(pyexecdir)
+# Dirs of external packages
+dbuspolicydir=@dbuspolicydir@
+dbussystemservicedir=@dbussystemservicedir@
+
CLEANFILES =
DISTCLEANFILES =
EXTRA_DIST =
$(MAKE) -C po update-po
DISTCHECK_CONFIGURE_FLAGS = \
+ --with-dbuspolicydir=$$dc_install_base/$(dbuspolicydir) \
+ --with-dbussystemservicedir=$$dc_install_base/$(dbussystemservicedir) \
--with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir)
# ------------------------------------------------------------------------------
collecty_PYTHON = \
src/collecty/__init__.py \
+ src/collecty/bus.py \
src/collecty/client.py \
src/collecty/constants.py \
src/collecty/daemon.py \
collectypluginsdir = $(collectydir)/plugins
+dist_dbuspolicy_DATA = \
+ src/dbus/org.ipfire.collecty1.conf
+
+dist_dbussystemservice_DATA = \
+ src/dbus/org.ipfire.collecty1.service
+
+systemdsystemunit_DATA = \
+ src/systemd/collecty.service
+
+dist_systemdsystemunit_DATA = \
+ src/systemd/org.ipfire.collecty1.busname
+
+EXTRA_DIST += \
+ src/systemd/collecty.service.in
+
+CLEANFILES += \
+ src/systemd/collecty.service
+
# ------------------------------------------------------------------------------
if ENABLE_MANPAGES
# ------------------------------------------------------------------------------
-if HAVE_SYSTEMD
-systemdsystemunit_DATA = \
- src/systemd/collecty.service
-
-CLEANFILES += \
- $(systemdsystemunit_DATA)
-
-INSTALL_DIRS += \
- $(systemdsystemunitdir)
-endif
-
-EXTRA_DIST += \
- src/systemd/collecty.service.in
-
-# ------------------------------------------------------------------------------
-
substitutions = \
'|PACKAGE_NAME=$(PACKAGE_NAME)|' \
'|PACKAGE_VERSION=$(PACKAGE_VERSION)|' \
AS_HELP_STRING([--with-systemd], [Enable systemd support.])
)
+AC_ARG_WITH([dbuspolicydir],
+ AS_HELP_STRING([--with-dbuspolicydir=DIR], [D-Bus policy directory]),
+ [],
+ [with_dbuspolicydir=${sysconfdir}/dbus-1/system.d]
+)
+
+AC_ARG_WITH([dbussystemservicedir],
+ AS_HELP_STRING([--with-dbussystemservicedir=DIR], [D-Bus system service directory]),
+ [],
+ [with_dbussystemservicedir=${datadir}/dbus-1/system-services]
+)
+
AS_IF([test "x$with_systemd" != "xno"],
[PKG_CHECK_MODULES(systemd, [libsystemd-daemon],
[have_systemd=yes], [have_systemd=no])],
AM_CONDITIONAL(HAVE_SYSTEMD, [test "x$have_systemd" = "xyes"])
+AC_SUBST([dbuspolicydir], [$with_dbuspolicydir])
+AC_SUBST([dbussystemservicedir], [$with_dbussystemservicedir])
+
# ------------------------------------------------------------------------------
AC_CONFIG_FILES([
${PACKAGE_NAME} ${VERSION}
prefix: ${prefix}
+ D-Bus policy dir: ${with_dbuspolicydir}
+ D-Bus system dir: ${with_dbussystemservicedir}
Systemd support ${have_systemd}
Generate man-pages: ${have_manpages}
+src/collecty/bus.py
src/collecty/client.py
src/collecty/constants.py
src/collecty/daemon.py
# #
###############################################################################
-import collecty
+import collecty.client
-client = collecty.CollectyClient()
-
-for ds in client.data_sources:
- for template in ds.templates:
- t = template(ds)
-
- for interval in ("-3h", "day", "week", "year"):
- t.graph("graphs/%s-%s-%s.png" % (t.name, ds.id, interval), interval)
+client = collecty.client.CollectyClient()
+client.run_cli()
--- /dev/null
+#!/usr/bin/python
+###############################################################################
+# #
+# collecty - A system statistics collection daemon for IPFire #
+# Copyright (C) 2015 IPFire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###############################################################################
+
+import dbus
+import dbus.mainloop.glib
+import dbus.service
+import gobject
+import threading
+
+from constants import *
+from i18n import _
+
+import logging
+log = logging.getLogger("collecty.bus")
+log.propagate = 1
+
+# Initialise the glib main loop
+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+dbus.mainloop.glib.threads_init()
+
+class Bus(threading.Thread):
+ def __init__(self, collecty):
+ threading.Thread.__init__(self)
+ self.daemon = True
+
+ self.collecty = collecty
+
+ # Initialise the main loop
+ gobject.threads_init()
+ self.loop = gobject.MainLoop()
+
+ # Register the GraphGenerator interface
+ self.generator = GraphGenerator(self.collecty)
+
+ def run(self):
+ log.debug(_("Bus thread has started"))
+
+ # Run the main loop
+ self.loop.run()
+
+ def shutdown(self):
+ log.debug(_("Stopping bus thread"))
+
+ # End the main loop
+ self.loop.quit()
+
+ # Return when this thread has finished
+ return self.join()
+
+
+class GraphGenerator(dbus.service.Object):
+ def __init__(self, collecty):
+ bus_name = dbus.service.BusName(BUS_DOMAIN, bus=dbus.SystemBus())
+ dbus.service.Object.__init__(self, bus_name, "/%s" % self.__class__.__name__)
+
+ self.collecty = collecty
+
+ @dbus.service.method(BUS_DOMAIN, in_signature="sa{sv}", out_signature="ay")
+ def GenerateGraph(self, template_name, kwargs):
+ """
+ Returns a graph generated from the given template and object.
+ """
+ graph = self.collecty.generate_graph(template_name, **kwargs)
+
+ return dbus.ByteArray(graph or [])
+
+ @dbus.service.method(BUS_DOMAIN, in_signature="", out_signature="as")
+ def ListTemplates(self):
+ """
+ Returns a list of all available templates
+ """
+ return [t.name for t in self.collecty.templates]
# #
###############################################################################
-import daemon
+import argparse
+import dbus
+import sys
+
+from constants import *
+from i18n import _
import logging
log = logging.getLogger("collectly.client")
class CollectyClient(object):
- def __init__(self, **settings):
- self.collecty = daemon.Collecty(**settings)
+ def __init__(self):
+ self.bus = dbus.SystemBus()
+
+ self.proxy = self.bus.get_object(BUS_DOMAIN, "/GraphGenerator")
+
+ def list_templates(self):
+ templates = self.proxy.ListTemplates()
+
+ return ["%s" % t for t in templates]
+
+ def list_templates_cli(self, ns):
+ templates = self.list_templates()
+
+ for t in sorted(templates):
+ print t
+
+ def generate_graph(self, template_name, **kwargs):
+ byte_array = self.proxy.GenerateGraph(template_name, kwargs,
+ signature="sa{sv}")
+
+ # Convert the byte array into a byte string again
+ if byte_array:
+ return "".join((chr(b) for b in byte_array))
+
+ def generate_graph_cli(self, ns):
+ kwargs = {
+ "object_id" : ns.object,
+ }
+
+ if ns.height or ns.width:
+ kwargs.update({
+ "height" : ns.height or 0,
+ "width" : ns.width or 0,
+ })
+
+ if ns.interval:
+ kwargs["interval"] = ns.interval
+
+ # Generate the graph image
+ graph = self.generate_graph(ns.template, **kwargs)
+
+ # Write file to disk
+ with open(ns.filename, "wb") as f:
+ f.write(graph)
+
+ def parse_cli(self, args):
+ parser = argparse.ArgumentParser(prog="collecty-client")
+ subparsers = parser.add_subparsers(help="sub-command help")
+
+ # generate-graph
+ parser_generate_graph = subparsers.add_parser("generate-graph",
+ help=_("Generate a graph image"))
+ parser_generate_graph.set_defaults(func=self.generate_graph_cli)
+ parser_generate_graph.add_argument("--filename",
+ help=_("filename"), required=True)
+ parser_generate_graph.add_argument("--interval", help=_("interval"))
+ parser_generate_graph.add_argument("--object",
+ help=_("Object identifier"), default="default")
+ parser_generate_graph.add_argument("--template",
+ help=_("The graph template identifier"), required=True)
- @property
- def data_sources(self):
- return self.collecty.data_sources
+ # Dimensions
+ parser_generate_graph.add_argument("--height", type=int, default=0,
+ help=_("Height of the generated image"))
+ parser_generate_graph.add_argument("--width", type=int, default=0,
+ help=_("Width of the generated image"))
- def get_data_source_by_id(self, id):
- for ds in self.data_sources:
- if not ds.id == id:
- continue
+ # list-templates
+ parser_list_templates = subparsers.add_parser("list-templates",
+ help=_("Lists all graph templates"))
+ parser_list_templates.set_defaults(func=self.list_templates_cli)
- return ds
+ return parser.parse_args(args)
- def graph(self, id, filename, interval=None, **kwargs):
- ds = self.get_data_source_by_id(id)
- assert ds, "Could not find data source: %s" % id
+ def run_cli(self, args=None):
+ args = self.parse_cli(args or sys.argv[1:])
- ds.graph(filename, interval=interval, **kwargs)
+ return args.func(args)
DATABASE_DIR = "/var/lib/collecty"
-GRAPH_DEFAULT_ARGUMENTS = [
+BUS_DOMAIN = "org.ipfire.collecty1"
+
+GRAPH_DEFAULT_ARGUMENTS = (
# Always generate graphs in PNG format.
"--imgformat", "PNG",
# Brand all generated graphs.
"--watermark", _("Created by collecty"),
-]
+)
GRAPH_DEFAULT_WIDTH = 768
GRAPH_DEFAULT_HEIGHT = 480
import threading
import time
+import bus
import plugins
from constants import *
# will be written to disk later.
self.write_queue = WriteQueue(self, self.SUBMIT_INTERVAL)
+ # Create a thread that connects to dbus and processes requests we
+ # get from there.
+ self.bus = bus.Bus(self)
+
# Add all plugins
for plugin in plugins.get():
self.add_plugin(plugin)
self.plugins.append(plugin)
+ @property
+ def templates(self):
+ for plugin in self.plugins:
+ for template in plugin.templates:
+ yield template
+
def run(self):
# Register signal handlers.
self.register_signal_handler()
+ # Start the bus
+ self.bus.start()
+
# Start all data source threads.
for p in self.plugins:
p.start()
for p in self.plugins:
p.join()
+ # Stop the bus thread
+ self.bus.shutdown()
+
# Write all collected data to disk before ending the main thread
self.write_queue.shutdown()
# Commit all data.
self.write_queue.commit()
- @property
- def graph_default_arguments(self):
- return GRAPH_DEFAULT_ARGUMENTS
+ def get_plugin_from_template(self, template_name):
+ for plugin in self.plugins:
+ if not template_name in [t.name for t in plugin.templates]:
+ continue
+
+ return plugin
+
+ def generate_graph(self, template_name, *args, **kwargs):
+ plugin = self.get_plugin_from_template(template_name)
+ if not plugin:
+ raise RuntimeError("Could not find template %s" % template_name)
+
+ return plugin.generate_graph(template_name, *args, **kwargs)
class WriteQueue(threading.Thread):
try:
return socket.gethostbyname(host)
- except PingResolvError:
- raise PingResolveError
+ except socket.gaierror as e:
+ if e.errno == -3:
+ raise PingResolveError
+
+ raise
def _is_valid_ipv4_address(self, addr):
"""
import math
import os
import rrdtool
+import tempfile
import threading
import time
if self.timer:
self.timer.cancel()
+ def get_object(self, id):
+ for object in self.objects:
+ if not object.id == id:
+ continue
+
+ return object
+
+ def get_template(self, template_name):
+ for template in self.templates:
+ if not template.name == template_name:
+ continue
+
+ return template(self)
+
+ def generate_graph(self, template_name, object_id="default", **kwargs):
+ template = self.get_template(template_name)
+ if not template:
+ raise RuntimeError("Could not find template %s" % template_name)
+
+ time_start = time.time()
+
+ graph = template.generate_graph(object_id=object_id, **kwargs)
+
+ duration = time.time() - time_start
+ self.log.info(_("Generated graph %s in %.1fms") \
+ % (template, duration * 1000))
+
+ return graph
+
class Object(object):
# The schema of the RRD database.
# Extra arguments passed to rrdgraph.
rrd_graph_args = []
- def __init__(self, ds):
- self.ds = ds
+ intervals = {
+ None : "-3h",
+ "hour" : "-1h",
+ "day" : "-25h",
+ "week" : "-360h",
+ "year" : "-365d",
+ }
+
+ # Default dimensions for this graph
+ height = GRAPH_DEFAULT_HEIGHT
+ width = GRAPH_DEFAULT_WIDTH
+
+ def __init__(self, plugin):
+ self.plugin = plugin
+
+ def __repr__(self):
+ return "<%s>" % self.__class__.__name__
@property
def collecty(self):
- return self.ds.collecty
+ return self.plugin.collecty
- def graph(self, file, interval=None,
- width=GRAPH_DEFAULT_WIDTH, height=GRAPH_DEFAULT_HEIGHT):
- args = [
- "--width", "%d" % width,
- "--height", "%d" % height,
+ @property
+ def log(self):
+ return self.plugin.log
+
+ def _make_command_line(self, interval, width=None, height=None):
+ args = []
+
+ args += GRAPH_DEFAULT_ARGUMENTS
+
+ args += [
+ "--height", "%s" % (height or self.height),
+ "--width", "%s" % (width or self.width),
]
- args += self.collecty.graph_default_arguments
- args += self.rrd_graph_args
- intervals = {
- None : "-3h",
- "hour" : "-1h",
- "day" : "-25h",
- "week" : "-360h",
- "year" : "-365d",
- }
+ args += self.rrd_graph_args
+ # Add interval
args.append("--start")
+
try:
- args.append(intervals[interval])
+ args.append(self.intervals[interval])
except KeyError:
- args.append(interval)
+ args.append(str(interval))
+
+ return args
+
+ def get_object_table(self, object_id):
+ return {
+ "file" : self.plugin.get_object(object_id),
+ }
+
+ def get_object_files(self, object_id):
+ files = {}
+
+ for id, obj in self.get_object_table(object_id).items():
+ files[id] = obj.file
+
+ return files
+
+ def generate_graph(self, object_id, interval=None, **kwargs):
+ args = self._make_command_line(interval, **kwargs)
+
+ self.log.info(_("Generating graph %s") % self)
+ self.log.debug(" args: %s" % args)
+
+ object_files = self.get_object_files(object_id)
- info = { "file" : self.ds.file }
for item in self.rrd_graph:
try:
- args.append(item % info)
+ args.append(item % object_files)
except TypeError:
args.append(item)
- rrdtool.graph(file, *args)
+ return self.write_graph(*args)
+
+ def write_graph(self, *args):
+ with tempfile.NamedTemporaryFile() as f:
+ rrdtool.graph(f.name, *args)
+
+ # Get back to the beginning of the file
+ f.seek(0)
+
+ # Return all the content
+ return f.read()
from ..i18n import _
-class GraphTemplateCPU(base.GraphTemplate):
- name = "cpu"
+class GraphTemplateProcessor(base.GraphTemplate):
+ name = "processor"
rrd_graph = [
"DEF:user=%(file)s:user:AVERAGE",
name = "processor"
description = "Processor Usage Plugin"
- templates = [GraphTemplateCPU,]
+ templates = [GraphTemplateProcessor]
interval = 30
name = "entropy"
description = "Entropy Plugin"
- templates = [GraphTemplateEntropy,]
+ templates = [GraphTemplateEntropy]
@property
def objects(self):
try:
ping = collecty.ping.Ping(destination=self.hostname, timeout=20000)
ping.run(count=5, deadline=self.deadline)
-
+
except collecty.ping.PingError, e:
self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
% { "host" : self.hostname, "msg" : e.msg })
name = "latency"
description = "Latency (ICMP ping) Plugin"
- templates = [GraphTemplateLatency,]
+ templates = [GraphTemplateLatency]
interval = 60
name = "loadavg"
description = "Load Average Plugin"
- templates = [GraphTemplateLoadAvg,]
+ templates = [GraphTemplateLoadAvg]
interval = 30
name = "memory"
description = "Memory Usage Plugin"
- templates = [GraphTemplateMemory,]
+ templates = [GraphTemplateMemory]
@property
def objects(self):
--- /dev/null
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!--
+ This file is part of collecty.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+-->
+
+<busconfig>
+ <policy user="root">
+ <allow own="org.ipfire.collecty1"/>
+ <allow send_destination="org.ipfire.collecty1"/>
+ <allow receive_sender="org.ipfire.collecty1"/>
+ </policy>
+
+ <policy context="default">
+ <allow send_destination="org.ipfire.collecty1"/>
+ <allow receive_sender="org.ipfire.collecty1"/>
+ </policy>
+</busconfig>
--- /dev/null
+[D-BUS Service]
+Name=org.ipfire.collecty1
+Exec=usr/bin/collectyd
+User=root
+SystemdService=collecty.service
Description=collecty - A system data collecting daemon
[Service]
+Type=dbus
ExecStart=@bindir@/collectyd
ExecReload=/bin/kill -HUP $MAINPID
+BusName=org.ipfire.collecty1
[Install]
WantedBy=multi-user.target
--- /dev/null
+[Unit]
+Description=Collecty Service Bus Name
+
+[BusName]
+Service=collecty.service
+AllowWorld=talk