Initial commit

master
Dustin C. Hatch 2016-09-15 23:26:59 -05:00
commit 2c2c55d0ef
9 changed files with 559 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/build/
/dist/
*.egg-info/
__pycache__/
*.py[co]

27
doc/conf.py Normal file
View File

@ -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,
}

7
doc/ifaddrs.rst Normal file
View File

@ -0,0 +1,7 @@
====================
``linuxapi.ifaddrs``
====================
.. automodule:: linuxapi.ifaddrs
:members:

18
doc/index.rst Normal file
View File

@ -0,0 +1,18 @@
Welcome to linuxapi's documentation!
====================================
Contents:
.. toctree::
:maxdepth: 2
:glob:
*
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

11
doc/inotify.rst Normal file
View File

@ -0,0 +1,11 @@
====================
``linuxapi.inotify``
====================
.. automodule:: linuxapi.inotify
:members:
:exclude-members: Event
.. autoclass:: Event
:no-members:

13
setup.py Normal file
View File

@ -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'},
)

0
src/linuxapi/__init__.py Normal file
View File

238
src/linuxapi/ifaddrs.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
'''\
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

240
src/linuxapi/inotify.py Normal file
View File

@ -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
'''