cryptonas-branches/pythonrewrite/bin2/CryptoBox.py

395 lines
14 KiB
Python
Executable File

#!/usr/bin/env python2.4
'''
This is the web interface for a fileserver managing encrypted filesystems.
It was originally written in bash/perl. Now a complete rewrite is in
progress. So things might be confusing here. Hopefully not for long.
:)
'''
# check python version
import sys
(ver_major, ver_minor, ver_sub, ver_desc, ver_subsub) = sys.version_info
if (ver_major < 2) or ((ver_major == 2) and (ver_minor < 4)):
sys.stderr.write("You need a python version >= 2.4\nCurrent version is:\n %s\n" % sys.version)
sys.exit(1)
import CryptoBoxContainer
import types
import re
import os
import unittest
import logging
import subprocess
CONF_LOCATIONS = [
"./cryptobox.conf",
"~/.cryptobox.conf",
"/etc/cryptobox/cryptobox.conf"]
class CryptoBox:
'''this class rules them all!
put things like logging, conf and oter stuff in here,
that might be used by more classes, it will be passed on to them'''
def __init__(self, config_file=None):
# choose an exit function
if __name__ == "__main__":
self.errorExit = self.errorExitProg
else:
self.errorExit = self.errorExitMod
self.__initLogging()
self.__initPreferences(config_file)
self.__runTests()
def __initLogging(self):
import logging
'''initialises the logging system
use it with: 'self.log.[debug|info|warning|error|critical](logmessage)'
all classes should get the logging instance during __init__:
self.log = logging.getLogger("CryptoBox")
first we output all warnings/errors to stderr
as soon as we opened the config file successfully, we redirect debug output
to the configured destination'''
## basicConfig(...) needs python >= 2.4
try:
self.log = logging.getLogger("CryptoBox")
logging.basicConfig(
format='%(asctime)s %(module)s %(levelname)s %(message)s',
stderr=sys.stderr)
self.log.setLevel(logging.ERROR)
self.log.info("loggingsystem is up'n running")
## from now on everything can be logged via self.log...
except:
self.errorExit("SystemError","Couldn't initialise the loggingsystem. I give up.")
def __initPreferences(self, config_file):
try:
import configobj ## needed for reading and writing of the config file
except:
self.errorExit("SystemError", "Couldn't import 'configobj'! Try 'apt-get install python-configobj'.")
# search for the configuration file
if config_file is None:
# no config file was specified - we will look for it in the ususal locations
conf_file_list = [os.path.expanduser(f)
for f in CONF_LOCATIONS
if os.path.exists(os.path.expanduser(f))]
if not conf_file_list:
# no possible config file found in the usual locations
self.errorExit("ConfigError", "No configuration file found - sorry!")
config_file = conf_file_list[0]
else:
# a config file was specified (e.g. via command line)
if type(config_file) != types.StringType:
self.errorExit("ConfigError","Invalid config file specified: %s" % config_file)
if not os.path.exists(config_file):
self.errorExit("ConfigError","Could not find the specified configuration file (%s)" % config_file)
try:
self.cbxPrefs = configobj.ConfigObj(config_file)
if self.cbxPrefs:
self.log.info("found config: %s" % self.cbxPrefs.items())
else:
self.errorExit("ConfigError","failed to load the config file: %s" % config_file)
except IOError:
self.errorExit("ConfigError","unable to open the config file: %s" % config_file)
try:
try:
nameDB_file = os.path.join(
self.cbxPrefs["Main"]["DataDir"],
self.cbxPrefs["Main"]["NameDatabase"])
except KeyError:
self.errorExit("ConfigError","could not find one of these configuration settings: [Main]->DataDir and [Main]->NameDatabase - please check your config file(%s)" % config_file)
except SyntaxError:
self.errorExit("ConfigError","Error during parsing of name database file (%s).\n" % (nameDB_file, ))
## create nameDB is necessary
if os.path.exists(nameDB_file):
self.nameDB = configobj.ConfigObj(nameDB_file)
else:
self.nameDB = configobj.ConfigObj(nameDB_file, create_empty=True)
## check if nameDB file was created successfully?
if not os.path.exists(nameDB_file):
self.errorExit("ConfigError","failed to create name database (%s)" % nameDB_file)
# get the loglevel
try:
log_level = self.cbxPrefs["Log"]["Level"].upper()
log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"]
if not log_level in log_level_avail:
self.errorExit("ConfigError","invalid log level: %s is not in %s" % (self.cbxPrefs["Log"]["Level"], log_level_avail))
except KeyError:
self.errorExit("ConfigError","could not find the configuration setting [Log]->Level in the config file (%s)" % config_file)
except TypeError:
self.errorExit("ConfigError","invalid log level: %s" % self.cbxPrefs["Log"]["Level"])
try:
try:
new_handler = logging.FileHandler(self.cbxPrefs["Log"]["Details"])
except KeyError:
self.errorExit("ConfigError","could not find a configuration setting: [Log]->Details - please check your config file(%s)" % config_file)
new_handler.setFormatter(logging.Formatter('%(asctime)s %(module)s %(levelname)s: %(message)s'))
self.log.addHandler(new_handler)
## do not call parent's handlers
self.log.propagate = False
## use 'getattr' as 'log_level' is a string
self.log.setLevel(getattr(logging,log_level))
except IOError:
self.errorExit("ConfigError","could not open logfile: %s" % self.cbxPrefs["Log"]["Details"])
# do some initial checks
def __runTests(self):
## try to run 'super' with 'CryptoBoxRootActions'
try:
devnull = open(os.devnull, "w")
except IOError:
self.errorExit("SystemError","Could not open %s for writing!" % os.devnull)
try:
proc = subprocess.Popen(
shell = False,
stdout = devnull,
stderr = devnull,
args = [ self.cbxPrefs["Programs"]["super"],
self.cbxPrefs["Programs"]["CryptoBoxRootActions"],
"check"])
except OSError:
self.errorExit("ConfigError","could not find: %s" % self.cbxPrefs["Programs"]["super"])
except KeyError:
self.errorExit("ConfigError","could not find one of these configurations settings: [Programs]->super or [Programs]->CryptoBoxRootActions in the config file")
proc.wait()
if proc.returncode != 0:
self.errorExit("ConfigError","Could not call CryptoBoxRootActions by 'super' - maybe you did not add the appropriate line to /etc/super.tab?")
# this method just demonstrates inheritance effects - may be removed
def cbx_inheritance_test(self, string="you lucky widow"):
self.log.info(string)
def errorExitMod(self, title, msg, code=1):
"""output an error message and quit by raising an exception
this function should be used if this class was used as a module instead
of as a standalone program
"""
self.log.error(msg)
raise title, msg
def errorExitProg(self, title, msg, code=1):
"""output an error message and quit by calling sys.exit
this function should be used if this class was used as a standalone
program
"""
self.log.error(msg)
sys.exit(code)
# RFC: why should CryptoBoxProps inherit CryptoBox? [l]
# RFC: shouldn't we move all useful functions of CryptoBoxProps to CryptoBox? [l]
class CryptoBoxProps(CryptoBox):
'''Get and set the properties of a CryptoBox
This class contains all available devices that may be accessed.
All properties of the cryptobox can be accessed by this class.
'''
def __init__(self, config_file=None):
'''read config and fill class variables'''
CryptoBox.__init__(self, config_file)
#self.cbx_inheritance_test()
#print self.cbxPrefs.items()
####
self.containers = []
for device in self.__getAvailablePartitions():
if self.isDeviceAllowed(device):
self.containers.append(CryptoBoxContainer.CryptoBoxContainer(device, self))
def isDeviceAllowed(self, devicename):
"check if a device is white-listed for being used as cryptobox containers"
allowed = self.cbxPrefs["Main"]["AllowedDevices"]
if type(allowed) == types.StringType: allowed = [allowed]
for a_dev in allowed:
"remove double dots and so on ..."
real_device = os.path.realpath(devicename)
if a_dev and re.search('^' + a_dev, real_device): return True
return False
def getLogData(self, lines=None, maxSize=None):
"""get the most recent log entries of the cryptobox
the maximum number and size of these entries can be limited by 'lines' and 'maxSize'
"""
# return nothing if the currently selected log output is not a file
empty = [""]
try:
if self.cbxPrefs["Log"]["Destination"].upper() != "FILE": return empty
log_file = self.cbxPrefs["Log"]["Details"]
except KeyError:
self.log.error("could not evaluate one of the following config settings: [Log]->Destination or [Log]->Details")
return empty
try:
fd = open(log_file, "r")
if maxSize: content = fd.readlines(maxSize)
else: content = fd.readlines()
fd.close()
except IOError:
self.log.warn("failed to read the log file (%s)" % log_file)
return empty
if lines: content = content[-lines:]
return content
def getContainerList(self, filterType=None, filterName=None):
"retrieve the list of all containers of this cryptobox"
try:
result = self.containers[:]
if filterType != None:
if filterType in range(len(CryptoBoxContainer.Types)):
return [e for e in self.containers if e.getType() == filterType]
else:
self.log.info("invalid filterType (%d)" % filterType)
result.clear()
if filterName != None:
result = [e for e in self.containers if e.getName() == filterName]
return result
except AttributeError:
return []
def setNameForUUID(self, uuid, name):
"assign a name to a uuid in the ContainerNameDatabase"
used_uuid = self.getUUIDForName(name)
"first remove potential conflicting uuid/name combination"
if used_uuid: del self.nameDB[used_uuid]
self.nameDB[uuid] = name
self.nameDB.write()
def getNameForUUID(self, uuid):
"get the name belonging to a specified key (usually the UUID of a fs)"
try:
return self.nameDB[uuid]
except KeyError:
return None
def getUUIDForName(self, name):
""" get the key belonging to a value in the ContainerNameDatabase
this is the reverse action of 'getNameForUUID' """
for key in self.nameDB.keys():
if self.nameDB[key] == name: return key
"the uuid was not found"
return None
""" ************ internal stuff starts here *********** """
def __getAvailablePartitions(self):
"retrieve a list of all available containers"
ret_list = []
try:
"the following reads all lines of /proc/partitions and adds the mentioned devices"
fpart = open("/proc/partitions", "r")
try:
line = fpart.readline()
while line:
p_details = line.split()
if (len(p_details) == 4):
"the following code prevents double entries like /dev/hda and /dev/hda1"
(p_major, p_minor, p_size, p_device) = p_details
if re.search('^[0-9]*$', p_major) and re.search('^[0-9]*$', p_minor):
p_parent = re.sub('[1-9]?[0-9]$', '', p_device)
if p_parent == p_device:
if [e for e in ret_list if re.search('^' + p_parent + '[1-9]?[0-9]$', e)]:
"major partition - its children are already in the list"
pass
else:
"major partition - but there are no children for now"
ret_list.append(p_device)
else:
"minor partition - remove parent if necessary"
if p_parent in ret_list: ret_list.remove(p_parent)
ret_list.append(p_device)
line = fpart.readline()
finally:
fpart.close()
return [self.__getAbsoluteDeviceName(e) for e in ret_list]
except IOError:
self.log.warning("Could not read /proc/partitions")
return []
def __getAbsoluteDeviceName(self, shortname):
""" returns the absolute file name of a device (e.g.: "hda1" -> "/dev/hda1")
this does also work for device mapper devices
if the result is non-unique, one arbitrary value is returned"""
if re.search('^/', shortname): return shortname
default = os.path.join("/dev", shortname)
if os.path.exists(default): return default
result = self.__findMajorMinorOfDevice(shortname)
"if no valid major/minor was found -> exit"
if not result: return default
(major, minor) = result
"for device-mapper devices (major == 254) ..."
if major == 254:
result = self.__findMajorMinorDeviceName("/dev/mapper", major, minor)
if result: return result[0]
"now check all files in /dev"
result = self.__findMajorMinorDeviceName("/dev", major, minor)
if result: return result[0]
return default
def __findMajorMinorOfDevice(self, device):
"return the major/minor numbers of a block device by querying /sys/block/?/dev"
if not os.path.exists(os.path.join("/sys/block", device)): return None
blockdev_info_file = os.path.join(os.path.join("/sys/block", device), "dev")
try:
f_blockdev_info = open(blockdev_info_file, "r")
blockdev_info = f_blockdev_info.read()
f_blockdev_info.close()
(str_major, str_minor) = blockdev_info.split(":")
"numeric conversion"
try:
major = int(str_major)
minor = int(str_minor)
return (major, minor)
except ValueError:
"unknown device numbers -> stop guessing"
return None
except IOError:
pass
def __findMajorMinorDeviceName(self, dir, major, minor):
"returns the names of devices with the specified major and minor number"
collected = []
try:
subdirs = [os.path.join(dir, e) for e in os.listdir(dir) if (not os.path.islink(os.path.join(dir, e))) and os.path.isdir(os.path.join(dir, e))]
"do a recursive call to parse the directory tree"
for dirs in subdirs:
collected.extend(self.__findMajorMinorDeviceName(dirs, major, minor))
"filter all device inodes in this directory"
collected.extend([os.path.realpath(os.path.join(dir, e)) for e in os.listdir(dir) if (os.major(os.stat(os.path.join(dir, e)).st_rdev) == major) and (os.minor(os.stat(os.path.join(dir, e)).st_rdev) == minor)])
result = []
for e in collected:
if e not in result: result.append(e)
return collected
except OSError:
return []
if __name__ == "__main__":
cb = CryptoBox()