From 2c2c55d0efa7e9311558683d71d53a6d240614e4 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 15 Sep 2016 23:26:59 -0500 Subject: [PATCH] Initial commit --- .gitignore | 5 + doc/conf.py | 27 +++++ doc/ifaddrs.rst | 7 ++ doc/index.rst | 18 +++ doc/inotify.rst | 11 ++ setup.py | 13 +++ src/linuxapi/__init__.py | 0 src/linuxapi/ifaddrs.py | 238 ++++++++++++++++++++++++++++++++++++++ src/linuxapi/inotify.py | 240 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 559 insertions(+) create mode 100644 .gitignore create mode 100644 doc/conf.py create mode 100644 doc/ifaddrs.rst create mode 100644 doc/index.rst create mode 100644 doc/inotify.rst create mode 100644 setup.py create mode 100644 src/linuxapi/__init__.py create mode 100644 src/linuxapi/ifaddrs.py create mode 100644 src/linuxapi/inotify.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f40058f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build/ +/dist/ +*.egg-info/ +__pycache__/ +*.py[co] diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..0bbaa38 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,27 @@ +import pkg_resources + +dist = pkg_resources.get_distribution('linuxapi') + +project = dist.project_name +version = release = dist.version +copyright = '2016, FireMon, LLC.' +author = 'Dustin C. Hatch' + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +language = None +exclude_patterns = ['_build'] +pygments_style = 'sphinx' +todo_include_todos = False +autodoc_member_order = 'groupwise' + +intersphinx_mapping = { + 'http://docs.python.org/': None, +} diff --git a/doc/ifaddrs.rst b/doc/ifaddrs.rst new file mode 100644 index 0000000..81b1b69 --- /dev/null +++ b/doc/ifaddrs.rst @@ -0,0 +1,7 @@ +==================== +``linuxapi.ifaddrs`` +==================== + + +.. automodule:: linuxapi.ifaddrs + :members: diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..b51880b --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,18 @@ +Welcome to linuxapi's documentation! +==================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + :glob: + + * + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/inotify.rst b/doc/inotify.rst new file mode 100644 index 0000000..8e820f0 --- /dev/null +++ b/doc/inotify.rst @@ -0,0 +1,11 @@ +==================== +``linuxapi.inotify`` +==================== + + +.. automodule:: linuxapi.inotify + :members: + :exclude-members: Event + + .. autoclass:: Event + :no-members: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0670ada --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from setuptools import find_packages, setup + +setup( + name='linuxapi', + version='1.0', + description='Pure Python bindings for Linux standard library features', + author='Dustin C. Hatch', + author_email='dustin.hatch@firemon.com', + url='https://www.firemon.com/', + license='GPL-2', + packages=find_packages('src'), + package_dir={'': 'src'}, +) diff --git a/src/linuxapi/__init__.py b/src/linuxapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linuxapi/ifaddrs.py b/src/linuxapi/ifaddrs.py new file mode 100644 index 0000000..472b337 --- /dev/null +++ b/src/linuxapi/ifaddrs.py @@ -0,0 +1,238 @@ +# Copyright 2016 FireMon. All rights reserved. +# +# 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 . +'''\ +This module provides a Pythonic interface to the Linux implementation of +:manpage:`getifaddrs(3)`. + +The :py:func:`getifaddrs` function returns a sequence of +:py:class:`Interface` objects representing the network interfaces on +the system. Each object has a list of :py:class:`InterfaceAddress` +objects that describe the addresses associated with the interface. + +At this time, only :py:data:`~socket.AF_INET` and +:py:data:`~socket.AF_INET6` addresses are supported, even though the +Linux implementation of :manpage:`getifaddrs(3)` may return more than +that (e.g. ``AF_PACKET``). +''' + +import ctypes.util +import os +import socket +import struct + + +__all__ = [ + 'IfaddrError', + 'Interface', + 'InterfaceAddress', + 'getifaddrs', +] + + +_libc = ctypes.CDLL(ctypes.util.find_library('c')) + + +_errno = _libc.__errno_location +_errno.restype = ctypes.POINTER(ctypes.c_int) + + + +class in_addr(ctypes.Structure): + + _fields_ = [ + ('s_addr', ctypes.c_byte * 4), + ] + + +class in6_addr(ctypes.Structure): + + _fields_ = [ + ('s_addr', ctypes.c_byte * 16), + ] + + +class sockaddr_in(ctypes.Structure): + + _fields_ = [ + ('sin_family', ctypes.c_ushort), + ('sin_port', ctypes.c_uint16), + ('sin_addr', in_addr), + ] + + +class sockaddr_in6(ctypes.Structure): + + _fields_ = [ + ('sin6_family', ctypes.c_ushort), + ('sin6_port', ctypes.c_uint16), + ('sin6_flowinfo', ctypes.c_uint32), + ('sin6_addr', in6_addr), + ('sin6_scope', ctypes.c_uint32), + ] + + +class sockaddr(ctypes.Structure): + + _fields_ = [ + ('sa_family', ctypes.c_ushort), + ('sa_data', ctypes.c_void_p), + ] + + +class ifa_ifu(ctypes.Union): + + _fields_ = [ + ('ifu_broadaddr', ctypes.POINTER(sockaddr)), + ('ifu_dstaddr', ctypes.POINTER(sockaddr)), + ] + + +class ifaddrs(ctypes.Structure): + pass + + +ifaddrs._fields_ = [ + ('ifa_next', ctypes.POINTER(ifaddrs)), + ('ifa_name', ctypes.c_char_p), + ('ifa_flags', ctypes.c_uint), + ('ifa_addr', ctypes.POINTER(sockaddr)), + ('ifa_netmask', ctypes.POINTER(sockaddr)), + ('ifa_ifu', ifa_ifu), + ('ifa_data', ctypes.c_void_p), +] + + +_libc.getifaddrs.argtypes = [ctypes.POINTER(ctypes.POINTER(ifaddrs))] +_libc.getifaddrs.restype = ctypes.c_int + +_libc.freeifaddrs.argtypes = [ctypes.POINTER(ifaddrs)] + + +class IfaddrError(OSError): + '''Raised when an error is returned by getifaddrs''' + + @classmethod + def from_c_err(cls): + errno = _errno().contents.value + return cls(errno, os.strerror(errno)) + + +class Interface(object): + '''A network interface''' + + __slots__ = ( + 'name', + 'addresses', + ) + + def __init__(self, name=None): + self.name = name + '''The name of the interface''' + self.addresses = [] + '''A list of addresses associated with the interface + + See :py:class:`InterfaceAddress` for details on the objects + contained in this list. + ''' + + +class InterfaceAddress(object): + '''An address associated with an interface''' + + __slots__ = ( + 'family', + 'addr', + 'netmask', + 'prefixlen', + ) + + def __init__(self): + self.family = None + '''The address family''' + self.addr = None + '''The numeric address''' + self.netmask = None + '''The subnet mask''' + self.prefixlen = None + '''The prefix length''' + + def __str__(self): + return '{}/{}'.format(self.addr, self.prefixlen) + + @classmethod + def from_sockaddr(cls, addr_p, netmask_p): + self = cls() + self.family = addr_p.contents.sa_family + if self.family == socket.AF_INET: + asin_p = ctypes.cast(addr_p, ctypes.POINTER(sockaddr_in)) + msin_p = ctypes.cast(netmask_p, ctypes.POINTER(sockaddr_in)) + abuf = bytes(bytearray(asin_p.contents.sin_addr.s_addr)) + mbuf = bytes(bytearray(msin_p.contents.sin_addr.s_addr)) + struct_fmt = 'I' + elif self.family == socket.AF_INET6: + asin6_p = ctypes.cast(addr_p, ctypes.POINTER(sockaddr_in6)) + msin6_p = ctypes.cast(netmask_p, ctypes.POINTER(sockaddr_in6)) + abuf = bytes(bytearray(asin6_p.contents.sin6_addr.s_addr)) + mbuf = bytes(bytearray(msin6_p.contents.sin6_addr.s_addr)) + struct_fmt = 'QQ' + else: + raise ValueError('Unsupported family {}'.format(self.family)) + self.addr = socket.inet_ntop(self.family, abuf) + self.netmask = socket.inet_ntop(self.family, mbuf) + self.prefixlen = popcount(sum(struct.unpack(struct_fmt, mbuf))) + return self + + +def getifaddrs(): + '''Get interface addresses + + :returns: A sequence of :py:class:`Interface` objects + :raises: :py:exc:`IfaddrError` + ''' + + ifaces = {} + ifa_p = ctypes.pointer(ifaddrs()) + rc = _libc.getifaddrs(ctypes.byref(ifa_p)) + if rc == -1: + raise IfaddrError.from_c_err() + if not ifa_p: + return [] + i = ifa_p.contents + while True: + try: + iface = ifaces[i.ifa_name] + except KeyError: + iface = ifaces[i.ifa_name] = Interface(i.ifa_name) + if i.ifa_addr and is_inet(i.ifa_addr.contents.sa_family): + addr = InterfaceAddress.from_sockaddr(i.ifa_addr, i.ifa_netmask) + iface.addresses.append(addr) + if not i.ifa_next: + break + i = i.ifa_next.contents + _libc.freeifaddrs(ifa_p) + return ifaces.values() + + +def is_inet(family): + return family in (socket.AF_INET, socket.AF_INET6) + + +def popcount(num): + c = 0 + v = num + while v: + v &= v - 1 + c += 1 + return c diff --git a/src/linuxapi/inotify.py b/src/linuxapi/inotify.py new file mode 100644 index 0000000..1fc7704 --- /dev/null +++ b/src/linuxapi/inotify.py @@ -0,0 +1,240 @@ +# Copyright 2016 FireMon. All rights reserved. +# +# This file is a part of the FireMon codebase. The contents of this +# file are confidential and cannot be distributed without prior +# written authorization. +# +# Warning: This computer program is protected by copyright law and +# international treaties. Unauthorized reproduction or distribution of +# this program, or any portion of it, may result in severe civil and +# criminal penalties, and will be prosecuted to the maximum extent +# possible under the law. +'''\ +This module provides Python bindings for the Linux inotify system, which +is a means for receiving events about file access and modification. + +All inotify operations are handled by the :py:class:`Inotify` class. +When an instance is created, the inotify system is initialized and an +inotify file descriptor is assigned. + +To begin watching files, use the :py:meth:`Inotify.add_watch` method. +Messages can then be obtained by iterating over the results of the +:py:meth:`Inotify.read` method. + +>>> inot = Inotify() +>>> inot.add_watch('/tmp', IN_CREATE | IN_MOVE) +>>> for event in inot.read(): +... print(event) + +The :py:meth:`Inotify.read` method will block until an event is +received. To avoid blocking, use an I/O multiplexing mechanism such as +:py:func:`~select.select` or :py:class:`~select.epoll`. +:py:class:`Inotify` instances can be passed directly to these +mechanisms, or the underlying file descriptor can be obtained by calling +the :py:meth:`Inotify.fileno` method. +''' + +import collections +import ctypes.util +import os +import struct + + +_libc = ctypes.CDLL(ctypes.util.find_library('c')) + +_errno = _libc.__errno_location +_errno.restype = ctypes.POINTER(ctypes.c_int) + +_libc.inotify_add_watch.argtypes = (ctypes.c_int, ctypes.c_char_p, + ctypes.c_uint32) +_libc.inotify_rm_watch.argtypes = (ctypes.c_int, ctypes.c_int) + +IN_NONBLOCK = 0x800 +IN_CLOEXEC = 0x80000 + +#: File was accessed. +IN_ACCESS = 0x1 +#: File was modified. +IN_MODIFY = 0x2 +#: Metadata changed. +IN_ATTRIB = 0x4 +#: Writtable file was closed. +IN_CLOSE_WRITE = 0x8 +#: Unwrittable file closed. +IN_CLOSE_NOWRITE = 0x10 +#: Close. +IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE +#: File was opened. +IN_OPEN = 0x20 +#: File was moved from X. +IN_MOVED_FROM = 0x40 +#: File was moved to Y. +IN_MOVED_TO = 0x80 +#: Moves. +IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO +#: Subfile was created. +IN_CREATE = 0x100 +#: Subfile was deleted. +IN_DELETE = 0x200 +#: Self was deleted. +IN_DELETE_SELF = 0x400 +#: Self was moved. +IN_MOVE_SELF = 0x800 + +#: Backing fs was unmounted. +IN_UNMOUNT = 0x2000 +#: Event queued overflowed. +IN_Q_OVERFLOW = 0x4000 +#: File was ignored. +IN_IGNORED = 0x8000 + +#: Only watch the path if it is a directory. +IN_ONLYDIR = 0x1000000 +#: DO not follow a sym link. +IN_DONT_FOLLOW = 0x2000000 +#: Exclude events on unlinked objects. +IN_EXCL_UNLINK = 0x4000000 +#: Add the mask of an already existing watch. +IN_MASK_ADD = 0x20000000 +#: Event occurred against dir. +IN_ISDIR = 0x40000000 +#: Only send event once. +IN_ONESHOT = 0x80000000 + +#: All events which a program can wait on. +IN_ALL_EVENTS = (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | + IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | IN_MOVED_TO | + IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF) + + +class InotifyError(OSError): + '''Raised when an error is returned by inotify''' + + @classmethod + def from_c_err(cls): + errno = _errno().contents.value + return cls(errno, os.strerror(errno)) + + +class Inotify(object): + '''Wrapper class for Linux inotify capabilities''' + + BUFSIZE = 4096 + STRUCT_FMT = '@iIII' + STRUCT_SIZE = struct.calcsize(STRUCT_FMT) + + def __init__(self): + fd = _libc.inotify_init() + if fd == -1: + raise InotifyError.from_c_err() + self.__fd = fd + self.__watches = {} + + def fileno(self): + '''Return the underlying inotify file descriptor''' + + return self.__fd + + def add_watch(self, pathname, mask): + '''Add a new watch + + :param pathname: The path to the file or directory to watch + :param mask: The events to watch, as a bit field + :returns: The new watch descriptor + :raises: :py:exc:`InotifyError` + ''' + + wd = _libc.inotify_add_watch(self.__fd, pathname, mask) + if wd == -1: + raise InotifyError.from_c_err() + self.__watches[wd] = pathname + return wd + + def rm_watch(self, wd): + '''Remove an existing watch + + :param wd: The watch descriptor to remove + :raises: :py:exc:`InotifyError` + ''' + + ret = _libc.inotify_rm_watch(self.__fd, wd) + if ret == -1: + raise InotifyError.from_c_err() + + def read(self): + '''Iterate over received events + + :returns: An iterator that yields :py:class:`Event` objects + + This method returns an iterator for all of the events received + in a single batch. Iterating over the returned value will block + until an event is received + ''' + + buf = memoryview(os.read(self.__fd, self.BUFSIZE)) + nread = len(buf) + i = 0 + while i < nread: + wd, mask, cookie, sz = self._unpack(buf) + end = self.STRUCT_SIZE + while end < nread and buf[end:end + 1] != b'\x00': + end += 1 + name = buf[self.STRUCT_SIZE:end].tobytes() + pathname = self.__watches[wd] + i += self.STRUCT_SIZE + sz + yield Event(wd, mask, cookie, name, pathname) + + def close(self): + '''Close all watch descriptors and the inotify descriptor''' + + for wd in self.__watches: + try: + self.rm_watch(wd) + except: + pass + os.close(self.__fd) + + @classmethod + def _unpack(cls, buf): + return struct.unpack(cls.STRUCT_FMT, buf[:cls.STRUCT_SIZE]) + + +Event = collections.namedtuple('Event', ( + 'wd', + 'mask', + 'cookie', + 'name', + 'pathname', +)) +'''A tuple containing information about a single event + +Each tuple contains the following items: + +.. py:attribute:: wd + + identifies the watch for which this event occurs. It is one of the + watch descriptors returned by a previous call to + :py:meth:`Inotify.add_watch` + +.. py:attribute:: mask + + contains bits that describe the event that occurred + +.. py:attribute:: cookie + + A unique integer that connects related events. Currently this is used + only for rename events, and allows the resulting pair of + :py:data:`IN_MOVED_FROM` and :py:data`IN_MOVED_TO` events to be + connected by the application. For all other event types, cookie is set + to ``0``. + +.. py:attribute:: name + + The ``name`` field is present only when an event is returned for a + file inside a watched directory; it identifies the filename within to + the watched directory. + +.. py:attribute:: pathname + + The path of the watched file or directory that emitted the event +'''