From: Zbigniew Jędrzejewski-Szmek Date: Fri, 1 Mar 2013 00:53:42 +0000 (-0500) Subject: Merge branch 'python-systemd-reader' X-Git-Tag: v198~140 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=elogind.git;a=commitdiff_plain;h=37d3ab1b7e114f0fb6dfb2e7273569b42794b76a;hp=54c31a79f72ff57ac8eba089acacc4ab482b745d Merge branch 'python-systemd-reader' * python-systemd-reader: python-systemd: rename Journal to Reader build-sys: upload python documentation to freedesktop.org systemd-python: add Journal class for reading journal python: build html docs using sphinx journalct: also print Python code in --new-id python: utilize uuid.UUID in logging python: add systemd.id128 module ... and 34 other commits In short: python module systemd.id128 is added, and existing systemd.journal gains a new class systemd.journal.Reader, which can be used to iterate over journal entries. Documentation is provided, and accessible under e.g. pydoc3 systemd.journal.Reader or firefox http://www.freedesktop.org/software/systemd/man/python-systemd/ --- diff --git a/Makefile.am b/Makefile.am index d00ca113e..790e5014b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3398,7 +3398,9 @@ EXTRA_DIST += \ # ------------------------------------------------------------------------------ if HAVE_PYTHON_DEVEL pkgpyexec_LTLIBRARIES = \ - _journal.la + _journal.la \ + id128.la \ + _reader.la _journal_la_SOURCES = \ src/python-systemd/_journal.c @@ -3418,11 +3420,75 @@ _journal_la_LIBADD = \ $(PYTHON_LIBS) \ libsystemd-journal.la +id128_la_SOURCES = \ + src/python-systemd/id128.c \ + src/python-systemd/id128-constants.h \ + src/python-systemd/pyutil.c \ + src/python-systemd/pyutil.h + +id128_la_CFLAGS = \ + $(AM_CFLAGS) \ + -fvisibility=default \ + $(PYTHON_CFLAGS) \ + -I$(top_builddir)/src/python-systemd + +id128_la_LDFLAGS = \ + $(AM_LDFLAGS) \ + -shared \ + -module \ + -avoid-version + +id128_la_LIBADD = \ + $(PYTHON_LIBS) \ + libsystemd-id128.la + +_reader_la_SOURCES = \ + src/python-systemd/_reader.c \ + src/python-systemd/pyutil.c \ + src/python-systemd/pyutil.h + +_reader_la_CFLAGS = \ + $(AM_CFLAGS) \ + -fvisibility=default \ + $(PYTHON_CFLAGS) + +_reader_la_LDFLAGS = \ + $(AM_LDFLAGS) \ + -shared \ + -module \ + -avoid-version + +_reader_la_LIBADD = \ + $(PYTHON_LIBS) \ + libsystemd-journal.la \ + libsystemd-id128.la \ + libsystemd-shared.la + dist_pkgpyexec_PYTHON = \ src/python-systemd/journal.py \ src/python-systemd/__init__.py + +src/python-systemd/id128-constants.h: src/systemd/sd-messages.h Makefile + $(AM_V_at)$(MKDIR_P) $(dir $@) + $(AM_V_GEN)$(SED) -n -r 's/,//g; s/#define (SD_MESSAGE_[A-Z0-9_]+)\s.*/add_id(m, "\1", \1) JOINER/p' <$< >$@ + +BUILT_SOURCES += \ + src/python-systemd/id128-constants.h endif +PAPER = $(shell cat /etc/papersize 2>/dev/null || echo a4) +SPHINXOPTS = -D latex_paper_size=$(PAPER) +sphinx-%: + $(AM_V_at)test -n "$(SPHINX_BUILD)" || { echo " *** sphinx-build is not available"; exit 1; } + $(AM_V_GEN)PYTHONPATH=$(DESTDIR)$(pyexecdir) LD_LIBRARY_PATH=$(DESTDIR)$(libdir) $(SPHINX_BUILD) -b $* $(SPHINXOPTS) $(top_srcdir)/src/python-systemd/docs $(top_builddir)/man/python-systemd/ + $(AM_V_at)echo Output has been generated in $(abs_top_builddir)/man/python-systemd/ + +destdir-sphinx: all + dir="`mktemp -d /tmp/systemd-install.XXXXXX`" && \ + $(MAKE) DESTDIR="$$dir" install && \ + $(MAKE) DESTDIR="$$dir" sphinx-html && \ + rm -rf "$$dir" + # ------------------------------------------------------------------------------ SED_PROCESS = \ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \ @@ -3743,17 +3809,19 @@ upload: all distcheck scp systemd-$(VERSION).tar.xz fdo:/srv/www.freedesktop.org/www/software/systemd/ scp man/*.html tango:public/systemd-man/ -doc-sync: all +www_target = www.freedesktop.org:/srv/www.freedesktop.org/www/software/systemd +doc-sync: all destdir-sphinx gtkdoc-rebase --html-dir=docs/libudev/html --online - rsync -av --delete docs/libudev/html/ --omit-dir-times www.freedesktop.org:/srv/www.freedesktop.org/www/software/systemd/libudev/ + rsync -av --delete docs/libudev/html/ --omit-dir-times $(www_target)/libudev/ gtkdoc-rebase --html-dir=docs/gudev/html --online - rsync -av --delete docs/gudev/html/ --omit-dir-times www.freedesktop.org:/srv/www.freedesktop.org/www/software/systemd/gudev/ - rsync -av --delete-excluded --include="*.html" --exclude="*" --omit-dir-times man/ www.freedesktop.org:/srv/www.freedesktop.org/www/software/systemd/man/ + rsync -av --delete docs/gudev/html/ --omit-dir-times $(www_target)/gudev/ + rsync -av --delete-excluded --include="*.html" --exclude="*" --omit-dir-times man/ $(www_target)/man/ + rsync -av --delete --omit-dir-times man/python-systemd/ $(www_target)/man/python-systemd/ git-tag: git tag "v$(VERSION)" -m "systemd $(VERSION)" install-tree: all rm -rf $(abs_srcdir)/install-tree - make install DESTDIR=$(abs_srcdir)/install-tree + $(MAKE) install DESTDIR=$(abs_srcdir)/install-tree tree $(abs_srcdir)/install-tree diff --git a/README b/README index 297d8f79e..1aa044be7 100644 --- a/README +++ b/README @@ -81,6 +81,7 @@ REQUIREMENTS: gperf gtkdocize (optional) python (optional) + sphinx (optional) When systemd-hostnamed is used it is strongly recommended to install nss-myhostname to ensure that in a world of @@ -93,6 +94,12 @@ REQUIREMENTS: please build D-Bus without systemd first, then build systemd, then rebuild D-Bus with systemd support. + To build HTML documentation for python-systemd using sphinx, + please first install systemd (using 'make install'), and then + invoke sphinx-build with 'make sphinx-', with + being 'html' or 'latexpdf'. If using DESTDIR for installation, + pass the same DESTDIR to 'make sphinx-html' invocation. + WARNINGS: systemd will warn you during boot if /etc/mtab is not a symlink to /proc/mounts. Please ensure that /etc/mtab is a diff --git a/configure.ac b/configure.ac index b7a587c66..ea9fafe2a 100644 --- a/configure.ac +++ b/configure.ac @@ -176,6 +176,7 @@ AS_IF([test "x$with_python" != "xno"], [ PYTHON_LIBS="`$PYTHON_CONFIG --ldflags`" AC_SUBST(PYTHON_CFLAGS) AC_SUBST(PYTHON_LIBS) + AC_PATH_PROGS(SPHINX_BUILD, sphinx-build-${PYTHON_VERSION} sphinx-build) ]) ]) AM_CONDITIONAL([HAVE_PYTHON_DEVEL], [test "$have_python_devel" = "yes"]) diff --git a/man/.gitignore b/man/.gitignore index 18767949c..fcf4dabd2 100644 --- a/man/.gitignore +++ b/man/.gitignore @@ -2,3 +2,4 @@ /systemd.index.xml /systemd.unit.xml /*.[13578] +/python-systemd/ diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 0b3a79bee..908450970 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -462,14 +462,17 @@ static int generate_new_id128(void) { "As UUID:\n" "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x\n\n" "As macro:\n" - "#define MESSAGE_XYZ SD_ID128_MAKE(", + "#define MESSAGE_XYZ SD_ID128_MAKE(", SD_ID128_FORMAT_VAL(id), SD_ID128_FORMAT_VAL(id)); - for (i = 0; i < 16; i++) printf("%02x%s", id.bytes[i], i != 15 ? "," : ""); + fputs(")\n\n", stdout); - fputs(")\n", stdout); + printf("As Python constant:\n" + ">>> import uuid\n" + ">>> MESSAGE_XYZ = uuid.UUID('" SD_ID128_FORMAT_STR "')\n", + SD_ID128_FORMAT_VAL(id)); return 0; } diff --git a/src/python-systemd/.gitignore b/src/python-systemd/.gitignore new file mode 100644 index 000000000..4124b7aff --- /dev/null +++ b/src/python-systemd/.gitignore @@ -0,0 +1,2 @@ +/id128-constants.h +*.py[oc] diff --git a/src/python-systemd/_journal.c b/src/python-systemd/_journal.c index 0bdf709ae..ced52b2f5 100644 --- a/src/python-systemd/_journal.c +++ b/src/python-systemd/_journal.c @@ -3,7 +3,7 @@ /*** This file is part of systemd. - Copyright 2012 David Strauss + Copyright 2012 David Strauss systemd is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by diff --git a/src/python-systemd/_reader.c b/src/python-systemd/_reader.c new file mode 100644 index 000000000..7f200d53f --- /dev/null +++ b/src/python-systemd/_reader.c @@ -0,0 +1,774 @@ +/*-*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2013 Steven Hiscocks, Zbigniew Jędrzejewski-Szmek + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include + +#include + +#include "pyutil.h" +#include "macro.h" +#include "util.h" + +#if PY_MAJOR_VERSION >=3 +# define unicode_FromStringAndSize PyUnicode_FromStringAndSize +# define unicode_FromString PyUnicode_FromString +# define long_FromLong PyLong_FromLong +# define long_FromSize_t PyLong_FromSize_t +# define long_Check PyLong_Check +# define long_AsLong PyLong_AsLong +#else +/* Python 3 type naming convention is used */ +# define unicode_FromStringAndSize PyString_FromStringAndSize +# define unicode_FromString PyString_FromString +# define long_FromLong PyInt_FromLong +# define long_FromSize_t PyInt_FromSize_t +# define long_Check PyInt_Check +# define long_AsLong PyInt_AsLong +#endif + +typedef struct { + PyObject_HEAD + sd_journal *j; +} Reader; +static PyTypeObject ReaderType; + +static int set_error(int r, const char* path, const char* invalid_message) { + if (r >= 0) + return r; + if (r == -EINVAL && invalid_message) + PyErr_SetString(PyExc_ValueError, invalid_message); + else if (r == -ENOMEM) + PyErr_SetString(PyExc_MemoryError, "Not enough memory"); + else { + errno = -r; + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + } + return 1; +} + +#if PY_MAJOR_VERSION >= 3 +static PyTypeObject MonotonicType; + +PyDoc_STRVAR(MonotonicType__doc__, + "A tuple of (timestamp, bootid) for holding monotonic timestamps"); + +static PyStructSequence_Field MonotonicType_fields[] = { + {(char*) "timestamp", (char*) "Time"}, + {(char*) "bootid", (char*) "Unique identifier of the boot"}, + {NULL, NULL} +}; + +static PyStructSequence_Desc Monotonic_desc = { + (char*) "journal.Monotonic", + MonotonicType__doc__, + MonotonicType_fields, + 2, +}; +#endif + +static void Reader_dealloc(Reader* self) +{ + sd_journal_close(self->j); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyDoc_STRVAR(Reader__doc__, + "Reader([flags][,path]) -> ...\n\n" + "Reader allows filtering and retrieval of Journal entries.\n" + "Argument `flags` sets open flags of the journal, which can be one\n" + "of, or ORed combination of constants: LOCAL_ONLY (default) opens\n" + "journal on local machine only; RUNTIME_ONLY opens only\n" + "volatile journal files; and SYSTEM_ONLY opens only\n" + "journal files of system services and the kernel.\n" + "Argument `path` is the directory of journal files. Note that\n" + "currently flags are ignored when `path` is present as they are\n" + "not relevant."); +static int Reader_init(Reader *self, PyObject *args, PyObject *keywds) +{ + int flags = SD_JOURNAL_LOCAL_ONLY, r; + char *path = NULL; + + static const char* const kwlist[] = {"flags", "path", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|iz", (char**) kwlist, + &flags, &path)) + return 1; + + Py_BEGIN_ALLOW_THREADS + if (path) + r = sd_journal_open_directory(&self->j, path, 0); + else + r = sd_journal_open(&self->j, flags); + Py_END_ALLOW_THREADS + + return set_error(r, path, "Invalid flags or path"); +} + +PyDoc_STRVAR(Reader_get_next__doc__, + "get_next([skip]) -> dict\n\n" + "Return dictionary of the next log entry. Optional skip value will\n" + "return the `skip`\\-th log entry."); +static PyObject* Reader_get_next(Reader *self, PyObject *args) +{ + PyObject *dict; + const void *msg; + size_t msg_len; + int64_t skip = 1LL; + int r; + + if (!PyArg_ParseTuple(args, "|L", &skip)) + return NULL; + + if (skip == 0LL) { + PyErr_SetString(PyExc_ValueError, "skip must be nonzero"); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + if (skip == 1LL) + r = sd_journal_next(self->j); + else if (skip == -1LL) + r = sd_journal_previous(self->j); + else if (skip > 1LL) + r = sd_journal_next_skip(self->j, skip); + else if (skip < -1LL) + r = sd_journal_previous_skip(self->j, -skip); + else + assert_not_reached("should not be here"); + Py_END_ALLOW_THREADS + + set_error(r, NULL, NULL); + if (r < 0) + return NULL; + else if (r == 0) /* EOF */ + return PyDict_New(); + + dict = PyDict_New(); + if (!dict) + return NULL; + + SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) { + PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL; + const char *delim_ptr; + + delim_ptr = memchr(msg, '=', msg_len); + if (!delim_ptr) { + PyErr_SetString(PyExc_OSError, + "journal gave us a field without '='"); + goto error; + } + + key = unicode_FromStringAndSize(msg, delim_ptr - (const char*) msg); + if (!key) + goto error; + + value = PyBytes_FromStringAndSize( + delim_ptr + 1, + (const char*) msg + msg_len - (delim_ptr + 1) ); + if (!value) + goto error; + + if (PyDict_Contains(dict, key)) { + PyObject *cur_value = PyDict_GetItem(dict, key); + + if (PyList_CheckExact(cur_value)) { + r = PyList_Append(cur_value, value); + if (r < 0) + goto error; + } else { + PyObject _cleanup_Py_DECREF_ *tmp_list = PyList_New(0); + if (!tmp_list) + goto error; + + r = PyList_Append(tmp_list, cur_value); + if (r < 0) + goto error; + + r = PyList_Append(tmp_list, value); + if (r < 0) + goto error; + + PyDict_SetItem(dict, key, tmp_list); + if (r < 0) + goto error; + } + } else { + r = PyDict_SetItem(dict, key, value); + if (r < 0) + goto error; + } + } + + { + PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL; + uint64_t realtime; + + r = sd_journal_get_realtime_usec(self->j, &realtime); + if (set_error(r, NULL, NULL)) + goto error; + + key = unicode_FromString("__REALTIME_TIMESTAMP"); + if (!key) + goto error; + + assert_cc(sizeof(unsigned long long) == sizeof(realtime)); + value = PyLong_FromUnsignedLongLong(realtime); + if (!value) + goto error; + + if (PyDict_SetItem(dict, key, value)) + goto error; + } + + { + PyObject _cleanup_Py_DECREF_ + *key = NULL, *timestamp = NULL, *bytes = NULL, *value = NULL; + sd_id128_t id; + uint64_t monotonic; + + r = sd_journal_get_monotonic_usec(self->j, &monotonic, &id); + if (set_error(r, NULL, NULL)) + goto error; + + assert_cc(sizeof(unsigned long long) == sizeof(monotonic)); + key = unicode_FromString("__MONOTONIC_TIMESTAMP"); + timestamp = PyLong_FromUnsignedLongLong(monotonic); + bytes = PyBytes_FromStringAndSize((const char*) &id.bytes, sizeof(id.bytes)); +#if PY_MAJOR_VERSION >= 3 + value = PyStructSequence_New(&MonotonicType); +#else + value = PyTuple_New(2); +#endif + if (!key || !timestamp || !bytes || !value) + goto error; + + Py_INCREF(timestamp); + Py_INCREF(bytes); + +#if PY_MAJOR_VERSION >= 3 + PyStructSequence_SET_ITEM(value, 0, timestamp); + PyStructSequence_SET_ITEM(value, 1, bytes); +#else + PyTuple_SET_ITEM(value, 0, timestamp); + PyTuple_SET_ITEM(value, 1, bytes); +#endif + + if (PyDict_SetItem(dict, key, value)) + goto error; + } + + { + PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL; + char _cleanup_free_ *cursor = NULL; + + r = sd_journal_get_cursor(self->j, &cursor); + if (set_error(r, NULL, NULL)) + goto error; + + key = unicode_FromString("__CURSOR"); + if (!key) + goto error; + + value = PyBytes_FromString(cursor); + if (!value) + goto error; + + if (PyDict_SetItem(dict, key, value)) + goto error; + } + + return dict; +error: + Py_DECREF(dict); + return NULL; +} + +PyDoc_STRVAR(Reader_get_previous__doc__, + "get_previous([skip]) -> dict\n\n" + "Return dictionary of the previous log entry. Optional skip value\n" + "will return the -`skip`\\-th log entry. Equivalent to get_next(-skip)."); +static PyObject* Reader_get_previous(Reader *self, PyObject *args) +{ + int64_t skip = 1LL; + if (!PyArg_ParseTuple(args, "|L", &skip)) + return NULL; + + return PyObject_CallMethod((PyObject *)self, (char*) "get_next", + (char*) "L", -skip); +} + +PyDoc_STRVAR(Reader_add_match__doc__, + "add_match(match) -> None\n\n" + "Add a match to filter journal log entries. All matches of different\n" + "fields are combined with logical AND, and matches of the same field\n" + "are automatically combined with logical OR.\n" + "Match is a string of the form \"FIELD=value\"."); +static PyObject* Reader_add_match(Reader *self, PyObject *args, PyObject *keywds) +{ + char *match; + int match_len, r; + if (!PyArg_ParseTuple(args, "s#", &match, &match_len)) + return NULL; + + r = sd_journal_add_match(self->j, match, match_len); + set_error(r, NULL, "Invalid match"); + if (r < 0) + return NULL; + + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_add_disjunction__doc__, + "add_disjunction() -> None\n\n" + "Inserts a logical OR between matches added before and afterwards."); +static PyObject* Reader_add_disjunction(Reader *self, PyObject *args) +{ + int r; + r = sd_journal_add_disjunction(self->j); + set_error(r, NULL, NULL); + if (r < 0) + return NULL; + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_flush_matches__doc__, + "flush_matches() -> None\n\n" + "Clear all current match filters."); +static PyObject* Reader_flush_matches(Reader *self, PyObject *args) +{ + sd_journal_flush_matches(self->j); + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_seek__doc__, + "seek(offset[, whence]) -> None\n\n" + "Jump `offset` entries in the journal. Argument\n" + "`whence` defines what the offset is relative to:\n" + "os.SEEK_SET (default) from first match in journal;\n" + "os.SEEK_CUR from current position in journal;\n" + "and os.SEEK_END is from last match in journal."); +static PyObject* Reader_seek(Reader *self, PyObject *args, PyObject *keywds) +{ + int64_t offset; + int whence = SEEK_SET; + PyObject *result = NULL; + + static const char* const kwlist[] = {"offset", "whence", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "L|i", (char**) kwlist, + &offset, &whence)) + return NULL; + + switch(whence) { + case SEEK_SET: { + int r; + Py_BEGIN_ALLOW_THREADS + r = sd_journal_seek_head(self->j); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + + if (offset > 0LL) + result = PyObject_CallMethod((PyObject *)self, (char*) "get_next", + (char*) "L", offset); + break; + } + case SEEK_CUR: + result = PyObject_CallMethod((PyObject *)self, (char*) "get_next", + (char*) "L", offset); + break; + case SEEK_END: { + int r; + Py_BEGIN_ALLOW_THREADS + r = sd_journal_seek_tail(self->j); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + + result = PyObject_CallMethod((PyObject *)self, (char*) "get_next", + (char*) "L", offset < 0LL ? offset : -1LL); + break; + } + default: + PyErr_SetString(PyExc_ValueError, "Invalid value for whence"); + } + + Py_XDECREF(result); + if (PyErr_Occurred()) + return NULL; + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_seek_realtime__doc__, + "seek_realtime(realtime) -> None\n\n" + "Seek to nearest matching journal entry to `realtime`. Argument\n" + "`realtime` can must be an integer unix timestamp."); +static PyObject* Reader_seek_realtime(Reader *self, PyObject *args) +{ + double timedouble; + uint64_t timestamp; + int r; + + if (!PyArg_ParseTuple(args, "d", &timedouble)) + return NULL; + + timestamp = (uint64_t) (timedouble * 1.0E6); + if ((int64_t) timestamp < 0LL) { + PyErr_SetString(PyExc_ValueError, "Time must be a positive integer"); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + r = sd_journal_seek_realtime_usec(self->j, timestamp); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_seek_monotonic__doc__, + "seek_monotonic(monotonic[, bootid]) -> None\n\n" + "Seek to nearest matching journal entry to `monotonic`. Argument\n" + "`monotonic` is an timestamp from boot in seconds.\n" + "Argument `bootid` is a string representing which boot the\n" + "monotonic time is reference to. Defaults to current bootid."); +static PyObject* Reader_seek_monotonic(Reader *self, PyObject *args) +{ + double timedouble; + char *bootid = NULL; + uint64_t timestamp; + sd_id128_t id; + int r; + + if (!PyArg_ParseTuple(args, "d|z", &timedouble, &bootid)) + return NULL; + + timestamp = (uint64_t) (timedouble * 1.0E6); + + if ((int64_t) timestamp < 0LL) { + PyErr_SetString(PyExc_ValueError, "Time must be positive number"); + return NULL; + } + + if (bootid) { + r = sd_id128_from_string(bootid, &id); + if (set_error(r, NULL, "Invalid bootid")) + return NULL; + } else { + Py_BEGIN_ALLOW_THREADS + r = sd_id128_get_boot(&id); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + r = sd_journal_seek_monotonic_usec(self->j, id, timestamp); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + Py_RETURN_NONE; +} + +PyDoc_STRVAR(Reader_wait__doc__, + "wait([timeout]) -> state change (integer)\n\n" + "Wait for a change in the journal. Argument `timeout` specifies\n" + "the maximum number of seconds to wait before returning\n" + "regardless of wheter the journal has changed. If `timeout` is not given\n" + "or is 0, then block forever.\n" + "Will return constants: NOP if no change; APPEND if new\n" + "entries have been added to the end of the journal; and\n" + "INVALIDATE if journal files have been added or removed."); +static PyObject* Reader_wait(Reader *self, PyObject *args, PyObject *keywds) +{ + int r; + int64_t timeout = 0LL; + + if (!PyArg_ParseTuple(args, "|L", &timeout)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + r = sd_journal_wait(self->j, timeout ==0 ? (uint64_t) -1 : timeout * 1E6); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, NULL)) + return NULL; + + return long_FromLong(r); +} + +PyDoc_STRVAR(Reader_seek_cursor__doc__, + "seek_cursor(cursor) -> None\n\n" + "Seek to journal entry by given unique reference `cursor`."); +static PyObject* Reader_seek_cursor(Reader *self, PyObject *args) +{ + const char *cursor; + int r; + + if (!PyArg_ParseTuple(args, "s", &cursor)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + r = sd_journal_seek_cursor(self->j, cursor); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, "Invalid cursor")) + return NULL; + Py_RETURN_NONE; +} + +static PyObject* Reader_iter(PyObject *self) +{ + Py_INCREF(self); + return self; +} + +static PyObject* Reader_iternext(PyObject *self) +{ + PyObject *dict; + Py_ssize_t dict_size; + + dict = PyObject_CallMethod(self, (char*) "get_next", (char*) ""); + if (PyErr_Occurred()) + return NULL; + dict_size = PyDict_Size(dict); + if ((int64_t) dict_size > 0LL) { + return dict; + } else { + Py_DECREF(dict); + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } +} + +PyDoc_STRVAR(Reader_query_unique__doc__, + "query_unique(field) -> a set of values\n\n" + "Return a set of unique values appearing in journal for the\n" + "given `field`. Note this does not respect any journal matches."); +static PyObject* Reader_query_unique(Reader *self, PyObject *args) +{ + char *query; + int r; + const void *uniq; + size_t uniq_len; + PyObject *value_set, *key, *value; + + if (!PyArg_ParseTuple(args, "s", &query)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + r = sd_journal_query_unique(self->j, query); + Py_END_ALLOW_THREADS + if (set_error(r, NULL, "Invalid field name")) + return NULL; + + value_set = PySet_New(0); + key = unicode_FromString(query); + + SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) { + const char *delim_ptr; + + delim_ptr = memchr(uniq, '=', uniq_len); + value = PyBytes_FromStringAndSize( + delim_ptr + 1, + (const char*) uniq + uniq_len - (delim_ptr + 1)); + PySet_Add(value_set, value); + Py_DECREF(value); + } + Py_DECREF(key); + return value_set; +} + +PyDoc_STRVAR(data_threshold__doc__, + "Threshold for field size truncation in bytes.\n\n" + "Fields longer than this will be truncated to the threshold size.\n" + "Defaults to 64Kb."); + +static PyObject* Reader_get_data_threshold(Reader *self, void *closure) +{ + size_t cvalue; + int r; + + r = sd_journal_get_data_threshold(self->j, &cvalue); + if (set_error(r, NULL, NULL)) + return NULL; + + return long_FromSize_t(cvalue); +} + +static int Reader_set_data_threshold(Reader *self, PyObject *value, void *closure) +{ + int r; + if (value == NULL) { + PyErr_SetString(PyExc_AttributeError, "Cannot delete data threshold"); + return -1; + } + if (!long_Check(value)){ + PyErr_SetString(PyExc_TypeError, "Data threshold must be an int"); + return -1; + } + r = sd_journal_set_data_threshold(self->j, (size_t) long_AsLong(value)); + return set_error(r, NULL, NULL); +} + +static PyGetSetDef Reader_getseters[] = { + {(char*) "data_threshold", + (getter) Reader_get_data_threshold, + (setter) Reader_set_data_threshold, + (char*) data_threshold__doc__, + NULL}, + {NULL} +}; + +static PyMethodDef Reader_methods[] = { + {"get_next", (PyCFunction) Reader_get_next, METH_VARARGS, Reader_get_next__doc__}, + {"get_previous", (PyCFunction) Reader_get_previous, METH_VARARGS, Reader_get_previous__doc__}, + {"add_match", (PyCFunction) Reader_add_match, METH_VARARGS|METH_KEYWORDS, Reader_add_match__doc__}, + {"add_disjunction", (PyCFunction) Reader_add_disjunction, METH_NOARGS, Reader_add_disjunction__doc__}, + {"flush_matches", (PyCFunction) Reader_flush_matches, METH_NOARGS, Reader_flush_matches__doc__}, + {"seek", (PyCFunction) Reader_seek, METH_VARARGS | METH_KEYWORDS, Reader_seek__doc__}, + {"seek_realtime", (PyCFunction) Reader_seek_realtime, METH_VARARGS, Reader_seek_realtime__doc__}, + {"seek_monotonic", (PyCFunction) Reader_seek_monotonic, METH_VARARGS, Reader_seek_monotonic__doc__}, + {"wait", (PyCFunction) Reader_wait, METH_VARARGS, Reader_wait__doc__}, + {"seek_cursor", (PyCFunction) Reader_seek_cursor, METH_VARARGS, Reader_seek_cursor__doc__}, + {"query_unique", (PyCFunction) Reader_query_unique, METH_VARARGS, Reader_query_unique__doc__}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject ReaderType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_reader._Reader", /*tp_name*/ + sizeof(Reader), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)Reader_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + Reader__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + Reader_iter, /* tp_iter */ + Reader_iternext, /* tp_iternext */ + Reader_methods, /* tp_methods */ + 0, /* tp_members */ + Reader_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc) Reader_init, /* tp_init */ + 0, /* tp_alloc */ + PyType_GenericNew, /* tp_new */ +}; + +#define SUMMARY \ + "Module that reads the systemd journal similar to journalctl." + +#if PY_MAJOR_VERSION >= 3 +static PyModuleDef _reader_module = { + PyModuleDef_HEAD_INIT, + "_reader", + SUMMARY, + -1, + NULL, NULL, NULL, NULL, NULL +}; +#endif + +#if PY_MAJOR_VERSION >= 3 +static bool initialized = false; +#endif + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" + +PyMODINIT_FUNC +#if PY_MAJOR_VERSION >= 3 +PyInit__reader(void) +#else +init_reader(void) +#endif +{ + PyObject* m; + + PyDateTime_IMPORT; + + if (PyType_Ready(&ReaderType) < 0) +#if PY_MAJOR_VERSION >= 3 + return NULL; +#else + return; +#endif + +#if PY_MAJOR_VERSION >= 3 + m = PyModule_Create(&_reader_module); + if (m == NULL) + return NULL; + + if (!initialized) { + PyStructSequence_InitType(&MonotonicType, &Monotonic_desc); + initialized = true; + } +#else + m = Py_InitModule3("_reader", NULL, SUMMARY); + if (m == NULL) + return; +#endif + + Py_INCREF(&ReaderType); +#if PY_MAJOR_VERSION >= 3 + Py_INCREF(&MonotonicType); +#endif + if (PyModule_AddObject(m, "_Reader", (PyObject *) &ReaderType) || +#if PY_MAJOR_VERSION >= 3 + PyModule_AddObject(m, "Monotonic", (PyObject*) &MonotonicType) || +#endif + PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP) || + PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND) || + PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE) || + PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY) || + PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY) || + PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY)) { +#if PY_MAJOR_VERSION >= 3 + Py_DECREF(m); + return NULL; +#endif + } + +#if PY_MAJOR_VERSION >= 3 + return m; +#endif +} + +#pragma GCC diagnostic pop diff --git a/src/python-systemd/docs/conf.py b/src/python-systemd/docs/conf.py new file mode 100644 index 000000000..4a55778b7 --- /dev/null +++ b/src/python-systemd/docs/conf.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# python-systemd documentation build configuration file, created by +# sphinx-quickstart on Sat Feb 9 13:49:42 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'python-systemd' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '197' +# The full version, including alpha/beta/rc tags. +release = '197' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'python-systemddoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'python-systemd.tex', u'python-systemd Documentation', + None, 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'python-systemd', u'python-systemd Documentation', + [], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'python-systemd', u'python-systemd Documentation', + u'David Strauss, Zbigniew Jędrzejewski-Szmek, Marti Raudsepp, Steven Hiscocks', 'python-systemd', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'python-systemd' +epub_author = u'David Strauss, Zbigniew Jędrzejewski-Szmek, Marti Raudsepp, Steven Hiscocks' +epub_publisher = u'David Strauss, Zbigniew Jędrzejewski-Szmek, Marti Raudsepp, Steven Hiscocks' +epub_copyright = u'2013, David Strauss, Zbigniew Jędrzejewski-Szmek, Marti Raudsepp, Steven Hiscocks' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/src/python-systemd/docs/id128.rst b/src/python-systemd/docs/id128.rst new file mode 100644 index 000000000..12c28f362 --- /dev/null +++ b/src/python-systemd/docs/id128.rst @@ -0,0 +1,38 @@ +`systemd.id128` module +====================== + +.. automodule:: systemd.id128 + :members: + :undoc-members: + :inherited-members: + + .. autoattribute:: systemd.id128.SD_MESSAGE_COREDUMP + .. autoattribute:: systemd.id128.SD_MESSAGE_FORWARD_SYSLOG_MISSED + .. autoattribute:: systemd.id128.SD_MESSAGE_HIBERNATE_KEY + .. autoattribute:: systemd.id128.SD_MESSAGE_JOURNAL_DROPPED + .. autoattribute:: systemd.id128.SD_MESSAGE_JOURNAL_MISSED + .. autoattribute:: systemd.id128.SD_MESSAGE_JOURNAL_START + .. autoattribute:: systemd.id128.SD_MESSAGE_JOURNAL_STOP + .. autoattribute:: systemd.id128.SD_MESSAGE_LID_CLOSED + .. autoattribute:: systemd.id128.SD_MESSAGE_LID_OPENED + .. autoattribute:: systemd.id128.SD_MESSAGE_OVERMOUNTING + .. autoattribute:: systemd.id128.SD_MESSAGE_POWER_KEY + .. autoattribute:: systemd.id128.SD_MESSAGE_SEAT_START + .. autoattribute:: systemd.id128.SD_MESSAGE_SEAT_STOP + .. autoattribute:: systemd.id128.SD_MESSAGE_SESSION_START + .. autoattribute:: systemd.id128.SD_MESSAGE_SESSION_STOP + .. autoattribute:: systemd.id128.SD_MESSAGE_SHUTDOWN + .. autoattribute:: systemd.id128.SD_MESSAGE_SLEEP_START + .. autoattribute:: systemd.id128.SD_MESSAGE_SLEEP_STOP + .. autoattribute:: systemd.id128.SD_MESSAGE_SPAWN_FAILED + .. autoattribute:: systemd.id128.SD_MESSAGE_STARTUP_FINISHED + .. autoattribute:: systemd.id128.SD_MESSAGE_SUSPEND_KEY + .. autoattribute:: systemd.id128.SD_MESSAGE_TIMEZONE_CHANGE + .. autoattribute:: systemd.id128.SD_MESSAGE_TIME_CHANGE + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_FAILED + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_RELOADED + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_RELOADING + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_STARTED + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_STARTING + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_STOPPED + .. autoattribute:: systemd.id128.SD_MESSAGE_UNIT_STOPPING diff --git a/src/python-systemd/docs/index.rst b/src/python-systemd/docs/index.rst new file mode 100644 index 000000000..f04d5a181 --- /dev/null +++ b/src/python-systemd/docs/index.rst @@ -0,0 +1,22 @@ +.. python-systemd documentation master file, created by + sphinx-quickstart on Sat Feb 9 13:49:42 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to python-systemd's documentation! +========================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + journal + id128 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/src/python-systemd/docs/journal.rst b/src/python-systemd/docs/journal.rst new file mode 100644 index 000000000..78b831aff --- /dev/null +++ b/src/python-systemd/docs/journal.rst @@ -0,0 +1,49 @@ +`systemd.journal` module +======================== + +.. automodule:: systemd.journal + :members: send, sendv, stream, stream_fd + :undoc-members: + +`JournalHandler` class +---------------------- + +.. autoclass:: JournalHandler + +Accessing the Journal +--------------------- + +.. autoclass:: _Reader + :undoc-members: + :inherited-members: + +.. autoclass:: Reader + :undoc-members: + :inherited-members: + + .. automethod:: __init__ + +.. autoclass:: Monotonic + +.. autoattribute:: systemd.journal.DEFAULT_CONVERTERS + +Whence constants +~~~~~~~~~~~~~~~~ + +.. autoattribute:: systemd.journal.SEEK_SET +.. autoattribute:: systemd.journal.SEEK_CUR +.. autoattribute:: systemd.journal.SEEK_END + +Journal access types +~~~~~~~~~~~~~~~~~~~~ + +.. autoattribute:: systemd.journal.LOCAL_ONLY +.. autoattribute:: systemd.journal.RUNTIME_ONLY +.. autoattribute:: systemd.journal.SYSTEM_ONLY + +Journal event types +~~~~~~~~~~~~~~~~~~~ + +.. autoattribute:: systemd.journal.NOP +.. autoattribute:: systemd.journal.APPEND +.. autoattribute:: systemd.journal.INVALIDATE diff --git a/src/python-systemd/id128.c b/src/python-systemd/id128.c new file mode 100644 index 000000000..a6711a5bd --- /dev/null +++ b/src/python-systemd/id128.c @@ -0,0 +1,162 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2013 Zbigniew Jędrzejewski-Szmek + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include + +#include + +#include + +#include "pyutil.h" + +PyDoc_STRVAR(module__doc__, + "Python interface to the libsystemd-id128 library.\n\n" + "Provides SD_MESSAGE_* constants and functions to query and generate\n" + "128-bit unique identifiers." +); + +PyDoc_STRVAR(randomize__doc__, + "randomize() -> UUID\n\n" + "Return a new random 128-bit unique identifier.\n" + "Wraps sd_id128_randomize(3)." +); + +PyDoc_STRVAR(get_machine__doc__, + "get_machine() -> UUID\n\n" + "Return a 128-bit unique identifier for this machine.\n" + "Wraps sd_id128_get_machine(3)." +); + +PyDoc_STRVAR(get_boot__doc__, + "get_boot() -> UUID\n\n" + "Return a 128-bit unique identifier for this boot.\n" + "Wraps sd_id128_get_boot(3)." +); + +static PyObject* make_uuid(sd_id128_t id) { + PyObject _cleanup_Py_DECREF_ + *uuid = NULL, *UUID = NULL, *bytes = NULL, + *args = NULL, *kwargs = NULL, *obj = NULL; + + uuid = PyImport_ImportModule("uuid"); + if (!uuid) + return NULL; + + UUID = PyObject_GetAttrString(uuid, "UUID"); + bytes = PyBytes_FromStringAndSize((const char*) &id.bytes, sizeof(id.bytes)); + args = Py_BuildValue("()"); + kwargs = PyDict_New(); + if (!UUID || !bytes || !args || !kwargs) + return NULL; + + if (PyDict_SetItemString(kwargs, "bytes", bytes) < 0) + return NULL; + + return PyObject_Call(UUID, args, kwargs); +} + +#define helper(name) \ + static PyObject *name(PyObject *self, PyObject *args) { \ + sd_id128_t id; \ + int r; \ + \ + assert(args == NULL); \ + \ + r = sd_id128_##name(&id); \ + if (r < 0) { \ + errno = -r; \ + return PyErr_SetFromErrno(PyExc_IOError); \ + } \ + \ + return make_uuid(id); \ + } + +helper(randomize) +helper(get_machine) +helper(get_boot) + +static PyMethodDef methods[] = { + { "randomize", randomize, METH_NOARGS, randomize__doc__}, + { "get_machine", get_machine, METH_NOARGS, get_machine__doc__}, + { "get_boot", get_boot, METH_NOARGS, get_boot__doc__}, + { NULL, NULL, 0, NULL } /* Sentinel */ +}; + +static int add_id(PyObject *module, const char* name, sd_id128_t id) { + PyObject *obj; + + obj = make_uuid(id); + if (!obj) + return -1; + + return PyModule_AddObject(module, name, obj); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" + +#if PY_MAJOR_VERSION < 3 + +PyMODINIT_FUNC initid128(void) { + PyObject *m; + + m = Py_InitModule3("id128", methods, module__doc__); + if (m == NULL) + return; + + /* a series of lines like 'add_id() ;' follow */ +#define JOINER ; +#include "id128-constants.h" +#undef JOINER +} + +#else + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "id128", /* name of module */ + module__doc__, /* module documentation, may be NULL */ + 0, /* size of per-interpreter state of the module */ + methods +}; + +PyMODINIT_FUNC PyInit_id128(void) { + PyObject *m; + + m = PyModule_Create(&module); + if (m == NULL) + return NULL; + + if ( /* a series of lines like 'add_id() ||' follow */ +#define JOINER || +#include "id128-constants.h" +#undef JOINER + false) { + Py_DECREF(m); + return NULL; + } + + return m; +} + +#endif + +#pragma GCC diagnostic pop diff --git a/src/python-systemd/journal.py b/src/python-systemd/journal.py index 516ca1ab5..23e1d6574 100644 --- a/src/python-systemd/journal.py +++ b/src/python-systemd/journal.py @@ -1,8 +1,10 @@ -# -*- Mode: python; indent-tabs-mode: nil -*- */ +# -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */ # # This file is part of systemd. # -# Copyright 2012 David Strauss +# Copyright 2012 David Strauss +# Copyright 2012 Zbigniew Jędrzejewski-Szmek +# Copyright 2012 Marti Raudsepp # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by @@ -17,12 +19,246 @@ # You should have received a copy of the GNU Lesser General Public License # along with systemd; If not, see . +from __future__ import division + +import sys as _sys +import datetime as _datetime +import functools as _functools +import uuid as _uuid import traceback as _traceback import os as _os +from os import SEEK_SET, SEEK_CUR, SEEK_END import logging as _logging +if _sys.version_info >= (3,): + from collections import ChainMap as _ChainMap from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG) from ._journal import sendv, stream_fd +from ._reader import (_Reader, NOP, APPEND, INVALIDATE, + LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY) +from . import id128 as _id128 + +if _sys.version_info >= (3,): + from ._reader import Monotonic +else: + Monotonic = tuple + +_MONOTONIC_CONVERTER = lambda p: Monotonic((_datetime.timedelta(microseconds=p[0]), + _uuid.UUID(bytes=p[1]))) +_REALTIME_CONVERTER = lambda x: _datetime.datetime.fromtimestamp(x / 1E6) +DEFAULT_CONVERTERS = { + 'MESSAGE_ID': _uuid.UUID, + '_MACHINE_ID': _uuid.UUID, + '_BOOT_ID': _uuid.UUID, + 'PRIORITY': int, + 'LEADER': int, + 'SESSION_ID': int, + 'USERSPACE_USEC': int, + 'INITRD_USEC': int, + 'KERNEL_USEC': int, + '_UID': int, + '_GID': int, + '_PID': int, + 'SYSLOG_FACILITY': int, + 'SYSLOG_PID': int, + '_AUDIT_SESSION': int, + '_AUDIT_LOGINUID': int, + '_SYSTEMD_SESSION': int, + '_SYSTEMD_OWNER_UID': int, + 'CODE_LINE': int, + 'ERRNO': int, + 'EXIT_STATUS': int, + '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER, + '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER, + '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER, + '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER, + 'COREDUMP': bytes, + 'COREDUMP_PID': int, + 'COREDUMP_UID': int, + 'COREDUMP_GID': int, + 'COREDUMP_SESSION': int, + 'COREDUMP_SIGNAL': int, + 'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER, +} + +if _sys.version_info >= (3,): + _convert_unicode = _functools.partial(str, encoding='utf-8') +else: + _convert_unicode = _functools.partial(unicode, encoding='utf-8') + +class Reader(_Reader): + """Reader allows the access and filtering of systemd journal + entries. Note that in order to access the system journal, a + non-root user must be in the `adm` group. + + Example usage to print out all error or higher level messages + for systemd-udevd for the boot: + + >>> myjournal = journal.Reader() + >>> myjournal.add_boot_match(journal.CURRENT_BOOT) + >>> myjournal.add_loglevel_matches(journal.LOG_ERR) + >>> myjournal.add_match(_SYSTEMD_UNIT="systemd-udevd.service") + >>> for entry in myjournal: + ... print(entry['MESSAGE']) + + See systemd.journal-fields(7) for more info on typical fields + found in the journal. + """ + def __init__(self, converters=None, flags=LOCAL_ONLY, path=None): + """Create an instance of Reader, which allows filtering and + return of journal entries. + Argument `converters` is a dictionary which updates the + DEFAULT_CONVERTERS to convert journal field values. + Argument `flags` sets open flags of the journal, which can be one + of, or ORed combination of constants: LOCAL_ONLY (default) opens + journal on local machine only; RUNTIME_ONLY opens only + volatile journal files; and SYSTEM_ONLY opens only + journal files of system services and the kernel. + Argument `path` is the directory of journal files. Note that + currently flags are ignored when `path` is present as they are + currently not relevant. + """ + super(Reader, self).__init__(flags, path) + if _sys.version_info >= (3,3): + self.converters = _ChainMap() + if converters is not None: + self.converters.maps.append(converters) + self.converters.maps.append(DEFAULT_CONVERTERS) + else: + self.converters = DEFAULT_CONVERTERS.copy() + if converters is not None: + self.converters.update(converters) + + def _convert_field(self, key, value): + """Convert value based on callable from self.converters + based of field/key""" + try: + result = self.converters[key](value) + except: + # Default conversion in unicode + try: + result = _convert_unicode(value) + except UnicodeDecodeError: + # Leave in default bytes + result = value + return result + + def _convert_entry(self, entry): + """Convert entire journal entry utilising _covert_field""" + result = {} + for key, value in entry.items(): + if isinstance(value, list): + result[key] = [self._convert_field(key, val) for val in value] + else: + result[key] = self._convert_field(key, value) + return result + + def add_match(self, *args, **kwargs): + """Add one or more matches to the filter journal log entries. + All matches of different field are combined in a logical AND, + and matches of the same field are automatically combined in a + logical OR. + Matches can be passed as strings of form "FIELD=value", or + keyword arguments FIELD="value". + """ + args = list(args) + args.extend(_make_line(key, val) for key, val in kwargs.items()) + for arg in args: + super(Reader, self).add_match(arg) + + def get_next(self, skip=1): + """Return the next log entry as a dictionary of fields. + + Optional skip value will return the `skip`\-th log entry. + + Entries will be processed with converters specified during + Reader creation. + """ + return self._convert_entry( + super(Reader, self).get_next(skip)) + + def query_unique(self, field): + """Return unique values appearing in the journal for given `field`. + + Note this does not respect any journal matches. + + Entries will be processed with converters specified during + Reader creation. + """ + return set(self._convert_field(field, value) + for value in super(Reader, self).query_unique(field)) + + def seek_realtime(self, realtime): + """Seek to a matching journal entry nearest to `realtime` time. + + Argument `realtime` must be either an integer unix timestamp + or datetime.datetime instance. + """ + if isinstance(realtime, _datetime.datetime): + realtime = float(realtime.strftime("%s.%f")) + return super(Reader, self).seek_realtime(realtime) + + def seek_monotonic(self, monotonic, bootid=None): + """Seek to a matching journal entry nearest to `monotonic` time. + + Argument `monotonic` is a timestamp from boot in either + seconds or a datetime.timedelta instance. Argument `bootid` + is a string or UUID representing which boot the monotonic time + is reference to. Defaults to current bootid. + """ + if isinstance(monotonic, _datetime.timedelta): + monotonic = monotonic.totalseconds() + if isinstance(bootid, _uuid.UUID): + bootid = bootid.get_hex() + return super(Reader, self).seek_monotonic(monotonic, bootid) + + def log_level(self, level): + """Set maximum log `level` by setting matches for PRIORITY. + """ + if 0 <= level <= 7: + for i in range(level+1): + self.add_match(PRIORITY="%s" % i) + else: + raise ValueError("Log level must be 0 <= level <= 7") + + def messageid_match(self, messageid): + """Add match for log entries with specified `messageid`. + + `messageid` can be string of hexadicimal digits or a UUID + instance. Standard message IDs can be found in systemd.id128. + + Equivalent to add_match(MESSAGE_ID=`messageid`). + """ + if isinstance(messageid, _uuid.UUID): + messageid = messageid.get_hex() + self.add_match(MESSAGE_ID=messageid) + + def this_boot(self, bootid=None): + """Add match for _BOOT_ID equal to current boot ID or the specified boot ID. + + If specified, bootid should be either a UUID or a 32 digit hex number. + + Equivalent to add_match(_BOOT_ID='bootid'). + """ + if bootid is None: + bootid = _id128.get_boot().hex + else: + bootid = getattr(bootid, 'hex', bootid) + self.add_match(_BOOT_ID=bootid) + + def this_machine(self, machineid=None): + """Add match for _MACHINE_ID equal to the ID of this machine. + + If specified, machineid should be either a UUID or a 32 digit hex number. + + Equivalent to add_match(_MACHINE_ID='machineid'). + """ + if machineid is None: + machineid = _id128.get_machine().hex + else: + machineid = getattr(machineid, 'hex', machineid) + self.add_match(_MACHINE_ID=machineid) + def _make_line(field, value): if isinstance(value, bytes): @@ -33,24 +269,18 @@ def _make_line(field, value): def send(MESSAGE, MESSAGE_ID=None, CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None, **kwargs): - r"""Send a message to journald. + r"""Send a message to the journal. >>> journal.send('Hello world') >>> journal.send('Hello, again, world', FIELD2='Greetings!') >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef') Value of the MESSAGE argument will be used for the MESSAGE= - field. + field. MESSAGE must be a string and will be sent as UTF-8 to + the journal. MESSAGE_ID can be given to uniquely identify the type of - message. - - Other parts of the message can be specified as keyword - arguments. - - Both MESSAGE and MESSAGE_ID, if present, must be strings, and - will be sent as UTF-8 to journal. Other arguments can be - bytes, in which case they will be sent as-is to journal. + message. It must be a string or a uuid.UUID object. CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to identify the caller. Unless at least on of the three is given, @@ -58,6 +288,11 @@ def send(MESSAGE, MESSAGE_ID=None, send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE must be an integer. + Additional fields for the journal entry can only be specified + as keyword arguments. The payload can be either a string or + bytes. A string will be sent as UTF-8, and bytes will be sent + as-is to the journal. + Other useful fields include PRIORITY, SYSLOG_FACILITY, SYSLOG_IDENTIFIER, SYSLOG_PID. """ @@ -65,7 +300,8 @@ def send(MESSAGE, MESSAGE_ID=None, args = ['MESSAGE=' + MESSAGE] if MESSAGE_ID is not None: - args.append('MESSAGE_ID=' + MESSAGE_ID) + id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID) + args.append('MESSAGE_ID=' + id) if CODE_LINE == CODE_FILE == CODE_FUNC == None: CODE_FILE, CODE_LINE, CODE_FUNC = \ @@ -94,19 +330,20 @@ def stream(identifier, priority=LOG_DEBUG, level_prefix=False): ', mode 'w' at 0x...> >>> stream.write('message...\n') - will produce the following message in the journal: + will produce the following message in the journal:: - PRIORITY=7 - SYSLOG_IDENTIFIER=myapp - MESSAGE=message... + PRIORITY=7 + SYSLOG_IDENTIFIER=myapp + MESSAGE=message... Using the interface with print might be more convinient: >>> from __future__ import print_function >>> print('message...', file=stream) - priority is the syslog priority, one of LOG_EMERG, LOG_ALERT, - LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG. + priority is the syslog priority, one of `LOG_EMERG`, + `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`, + `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`. level_prefix is a boolean. If true, kernel-style log priority level prefixes (such as '<1>') are interpreted. See @@ -120,7 +357,7 @@ class JournalHandler(_logging.Handler): """Journal handler class for the Python logging framework. Please see the Python logging module documentation for an - overview: http://docs.python.org/library/logging.html + overview: http://docs.python.org/library/logging.html. To create a custom logger whose messages go only to journal: @@ -129,31 +366,31 @@ class JournalHandler(_logging.Handler): >>> log.addHandler(journal.JournalHandler()) >>> log.warn("Some message: %s", detail) - Note that by default, message levels INFO and DEBUG are ignored - by the logging framework. To enable those log levels: + Note that by default, message levels `INFO` and `DEBUG` are + ignored by the logging framework. To enable those log levels: >>> log.setLevel(logging.DEBUG) To attach journal MESSAGE_ID, an extra field is supported: - >>> log.warn("Message with ID", - >>> extra={'MESSAGE_ID': '22bb01335f724c959ac4799627d1cb61'}) + >>> import uuid + >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF') + >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid}) To redirect all logging messages to journal regardless of where they come from, attach it to the root logger: >>> logging.root.addHandler(journal.JournalHandler()) - For more complex configurations when using dictConfig or - fileConfig, specify 'systemd.journal.JournalHandler' as the + For more complex configurations when using `dictConfig` or + `fileConfig`, specify `systemd.journal.JournalHandler` as the handler class. Only standard handler configuration options - are supported: level, formatter, filters. + are supported: `level`, `formatter`, `filters`. The following journal fields will be sent: - - MESSAGE, PRIORITY, THREAD_NAME, CODE_FILE, CODE_LINE, - CODE_FUNC, LOGGER (name as supplied to getLogger call), - MESSAGE_ID (optional, see above). + `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`, + `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call), + `MESSAGE_ID` (optional, see above). """ def emit(self, record): diff --git a/src/python-systemd/pyutil.c b/src/python-systemd/pyutil.c new file mode 100644 index 000000000..79065a11c --- /dev/null +++ b/src/python-systemd/pyutil.c @@ -0,0 +1,30 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2013 Zbigniew Jędrzejewski-Szmek + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include "pyutil.h" + +void cleanup_Py_DECREFp(PyObject **p) { + if (!*p) + return; + + Py_DECREF(*p); +} diff --git a/src/python-systemd/pyutil.h b/src/python-systemd/pyutil.h new file mode 100644 index 000000000..3b7bc580d --- /dev/null +++ b/src/python-systemd/pyutil.h @@ -0,0 +1,29 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2013 Zbigniew Jędrzejewski-Szmek + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#ifndef Py_TYPE +/* avoid duplication warnings from errors in Python 2.7 headers */ +# include +#endif + +void cleanup_Py_DECREFp(PyObject **p); + +#define _cleanup_Py_DECREF_ __attribute__((cleanup(cleanup_Py_DECREFp)))