changed the interface of CryptoBoxRootActions: "allowedProgs" are now prefixed with the parameter "program"

added allowedProg "pvdisplay" to CryptoBoxRootActions to allow LVM detection
improved blockdevice handling: caching and detection of lvm, luks and raid
This commit is contained in:
lars 2007-08-16 16:13:04 +00:00
parent 53e09ff825
commit b72310097c
5 changed files with 470 additions and 85 deletions

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python
#
# Copyright 2006 sense.lab e.V.
# Copyright 2007 sense.lab e.V.
#
# This file is part of the CryptoBox.
#
@ -20,13 +20,23 @@
#
"""module for executing the programs, that need root privileges
"""module for executing some programs or scripts that need root privileges
Syntax:
- TODO
check
- return exitcode zero if basic checks succeeded
this script will always return with an exitcode 0 (true),
if "check" is the only argument
program PROGRAM_NAME [ARGS]
- call the program (must be defined in "allowedProgs" below)
event EVENT_SCRIPT [ARGS]
- call an event script
plugin PLUGIN_NAME [ARGS]
- call a root_action script of a plugin
For more detailed information take a look at the manpage:
"man CryptoBoxRootActions"
"""
__revision__ = "$Id"
@ -44,6 +54,7 @@ allowedProgs = {
"mount": "/bin/mount",
"umount": "/bin/umount",
"blkid": "/sbin/blkid",
"pvdisplay": "/sbin/pvdisplay",
}
## this line is necessary for running unittests or playing around with a local
@ -387,6 +398,19 @@ def run_umount(args):
return proc.returncode == 0
def run_pvdisplay(args):
"""execute pvdisplay to check for physical LVM devices
"""
if len(args) > 0:
raise "WrongArguments", "no arguments may be supplied for 'pvdisplay'"
## call pvdisplay with the parameter "--colon"
proc = subprocess.Popen(
shell = False,
args = [ allowedProgs["pvdisplay"], "--colon" ])
proc.wait()
return proc.returncode == 0
def getCallingUserInfo():
"""return information about the user that was calling this program via "super"
@ -456,6 +480,7 @@ if __name__ == "__main__":
# exit silently
sys.exit(0)
## call a plugin root_action script
if args[0].lower() == "plugin":
del args[0]
try:
@ -468,6 +493,7 @@ if __name__ == "__main__":
else:
sys.exit(1)
## call an event script
if args[0].lower() == "event":
del args[0]
try:
@ -480,11 +506,13 @@ if __name__ == "__main__":
else:
sys.exit(1)
# check parameters count
if len(args) < 2:
sys.stderr.write("Not enough arguments supplied (%s)!\n" % " ".join(args))
sys.exit(100)
## call one of the allowed programs
if args[0].lower() == "program":
del args[0]
if len(args) < 1:
sys.stderr.write("No program specified for execution\n")
sys.exit(100)
progRequest = args[0]
del args[0]
@ -495,6 +523,7 @@ if __name__ == "__main__":
if progRequest == "cryptsetup": runner = run_cryptsetup
elif progRequest == "mount": runner = run_mount
elif progRequest == "umount": runner = run_umount
elif progRequest == "pvdisplay": runner = run_pvdisplay
else:
sys.stderr.write("The interface for this program (%s) is " \
+ "not yet implemented!\n" % progRequest)

View file

@ -1,4 +1,4 @@
.TH CryptoBoxRootActions 8 "March 02007" "CryptoBox" "CryptoBox-Server manual"
.TH CryptoBoxRootActions 8 "August 02007" "CryptoBox" "CryptoBox-Server manual"
.SH NAME
CryptoBoxRootActions \- The CryptoBoxWebserver calls this script in order to
execute various programs which require root privileges.
@ -13,7 +13,7 @@ plugin \fIFEATURE_SCRIPT\fR [\fIARGS\fR]
hook \fIEVENT_SCRIPT\fR [\fIARGS\fR]
.br
.B CryptoBoxRootActions
\fIPROG\fR [\fIARGS\fR]
program \fIPROG\fR [\fIARGS\fR]
.SH DESCRIPTION
CryptoBoxRootActions is a script that is called by the
\fBCryptoBox\fR-Server to execute programs which require root privileges. You
@ -30,7 +30,8 @@ CryptoBoxRootActions /usr/sbin/CryptoBoxRootActions cryptobox
.PP
We assume that the CryptoBoxRootActions script is located at
\fI/usr/sbin/CryptoBoxRootActions\fR. Furthermore the user running the
CryptoBox-Server is assumed to be \fIcryptobox\fR.
CryptoBox-Server is assumed to be \fIcryptobox\fR. Otherwise you must change the
above line accordingly.
.SH CONFIGURATION CHECK
Call the CryptoBoxRootActions script with the argument \fIcheck\fR to test if
\fBsuper\fR is configured properly. Just type the following:

View file

@ -25,26 +25,44 @@ These classes detect and filter available blockdevices.
__revision__ = "$Id$"
"""
TODO:
- implement some caching
- find the devnodes for each device (e.g. /dev/hda)
- detect luks devices
- detect LVM
"""
#TODO: use logger to report interesting behaviour
import os
import subprocess
import time
import cryptobox.core.settings
DEFAULT_SYSBLOCK_DIR = '/sys/block'
DEFAULT_DEVNODE_DIR = '/dev'
MINIMUM_STORAGE_SIZE = 20
MAJOR_DEVNUM_RAM = 1
MAJOR_DEVNUM_LOOP = 7
MAJOR_DEVNUM_MD_RAID = 9
USE_CACHE = True
CACHE_EXPIRE_SECONDS = 60
#TODO: remove this after profiling
IS_VISIBLE = True
## caching is quite important for the following implementation
CACHED_VALUES = {}
class Blockdevices:
"""handle all blockdevices of this system
"""
def __init__(self, sysblock_dir='/sys/block', devnode_dir='/dev'):
def __init__(self,
sysblock_dir=DEFAULT_SYSBLOCK_DIR,
devnode_dir=DEFAULT_DEVNODE_DIR):
self.sysblock_dir = sysblock_dir
self.devnode_dir = devnode_dir
self.devices = []
for devdir in find_blockdevices(self.sysblock_dir):
blockdevice = Blockdevice(devdir, self.devnode_dir)
if not blockdevice is None:
blockdevice = get_blockdevice(devdir,
self.sysblock_dir, self.devnode_dir)
if (not blockdevice is None) and blockdevice.is_valid():
self.devices.append(blockdevice)
@ -55,60 +73,197 @@ class Blockdevices:
class Blockdevice:
def __init__(self, dev, devnode_dir='/dev', sysblock_dir='/sys/block'):
def __init__(self, dev,
sysblock_dir=DEFAULT_SYSBLOCK_DIR,
devnode_dir=DEFAULT_DEVNODE_DIR):
"""initialize the blockdevice
"""
self.devdir = dev
self.devnode_dir = devnode_dir
self.sysblock_dir = sysblock_dir
if os.path.isabs(dev):
self.devdir = dev
else:
self.devdir = self.__find_relative_device(dev)
if self.devdir is None:
self = None
return
self.name = os.path.basename(self.devdir)
self.devnum = self.__get_major_minor()
## check valid devnum
try:
major, minor = self.devnum
except TypeError:
self = None
return
self.size = self.__get_size()
self.range = self.__get_device_range()
self.slaves = self.__get_dev_related("slaves")
self.holders = self.__get_dev_related("holders")
self.children = self.__get_children()
self.devnodes = self.__get_device_nodes()
def isstorage(self):
if self.range > 1:
## partitionable blockdevice
def is_valid(self):
""" check if the device is usable and valid
"""
if not self.devnodes:
return False
if self.size < 20:
## extended partition, unused loop device
return False
if self.devnum[0] == 1:
## ram device
return False
if self.children:
## a parent blockdevice
## check valid devnum
try:
major, minor = self.devnum
if (major == 0) and (minor == 0):
return False
## ram devices are ignored
if major == MAJOR_DEVNUM_RAM:
return False
## loop devices are ignored
if major == MAJOR_DEVNUM_LOOP:
return False
except TypeError:
return False
return True
def ispartitionable(self):
def is_storage(self):
"""return if this device can be used as a storage
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_storage"]
cached = _get_cached_value(cache_link)
if not cached is None:
return cached
if self.range > 1:
## partitionable blockdevice
_set_cached_value(cache_link, False)
return False
if self.size < MINIMUM_STORAGE_SIZE:
## extended partition, unused loop device
_set_cached_value(cache_link, False)
return False
if self.devnum[0] == MAJOR_DEVNUM_RAM:
## ram device
_set_cached_value(cache_link, False)
return False
## are we the device mapper of a luks device?
for slave in self.slaves:
if get_blockdevice(slave, self.sysblock_dir,
self.devnode_dir).is_luks():
_set_cached_value(cache_link, False)
return False
## if we are a luks device with exactly one child, then
## we are a storage
if (len(self.children) == 1) and self.is_luks():
_set_cached_value(cache_link, True)
return True
if self.children:
## a parent blockdevice
_set_cached_value(cache_link, False)
return False
_set_cached_value(cache_link, True)
return True
def is_partitionable(self):
"""is the device partitionable
"""
if self.range > 1:
return True
else:
return False
def __find_relative_device(self, devname):
for devdir in find_blockdevices(self.sysblock_dir):
if os.path.basename(devdir) == devname:
return devdir
return None
def is_lvm_pv(self):
"""return if the device is a physical volume of a LVM
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_lvm_pv"]
cached = _get_cached_value(cache_link)
if not cached is None:
return cached
## is one of the devnodes of the device a physical volume?
for one_lvm_pv in find_lvm_pv():
if one_lvm_pv in self.devnodes:
_set_cached_value(cache_link, True)
return True
_set_cached_value(cache_link, False)
return False
def is_lvm_lv(self):
"""return if the device is a logical volume of a LVM
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_lvm_lv"]
cached = _get_cached_value(cache_link)
if not cached is None:
return cached
## is one of the devnodes of the device a physical volume?
## logical LVM volumes always depend on their physical volumes
if not self.slaves:
_set_cached_value(cache_link, False)
return False
## is one of the LVM physical volumes a device node of our slave(s)?
for one_lvm_pv in find_lvm_pv():
for one_slave in self.slaves:
if one_lvm_pv in get_blockdevice(one_slave,
self.sysblock_dir, self.devnode_dir).devnodes:
_set_cached_value(cache_link, True)
return True
_set_cached_value(cache_link, False)
return False
def is_md_raid(self):
"""check if the device is the base of a md raid device
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_md_raid"]
cached = _get_cached_value(cache_link)
if not cached is None:
return cached
if self.range > 1:
result = False
elif self.size < MINIMUM_STORAGE_SIZE:
result = False
else:
for hold in self.holders:
if get_blockdevice(hold, self.sysblock_dir,
self.devnode_dir).devnum[0] == MAJOR_DEVNUM_MD_RAID:
result = True
break
else:
result = False
## store result and return
_set_cached_value(cache_link, result)
return result
def is_luks(self):
"""check if the device is a luks container
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_luks"]
cached = _get_cached_value(cache_link)
if not cached is None:
return cached
if self.range > 1:
result = False
elif self.size < MINIMUM_STORAGE_SIZE:
result = False
elif self.is_lvm_pv():
result = False
elif self.is_md_raid():
result = False
else:
## is the device a luks volume?
prefs = _load_preferences()
proc = subprocess.Popen(
shell = False,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [ prefs["Programs"]["cryptsetup"],
"--batch-mode", "isLuks", self.devnodes[0]])
proc.wait()
result = proc.returncode == 0
## store result and return
_set_cached_value(cache_link, result)
return result
def __get_dev_related(self, subdir):
@ -121,6 +276,8 @@ class Blockdevice:
def __get_size(self):
"""return the size (in kB) of the blockdevice
"""
default = 0
try:
return int(file(os.path.join(self.devdir, 'size')).read())
@ -132,12 +289,15 @@ class Blockdevice:
def __get_major_minor(self):
"""return the major and minor of the device"""
default = (0 ,0)
default = (0, 0)
try:
content = file(os.path.join(self.devdir, "dev")).read()
except IOError:
return default
major, minor = content.split(":", 2)
try:
major, minor = content.split(":", 1)
except TypeError:
return default
try:
return int(major), int(minor)
except ValueError:
@ -165,20 +325,48 @@ class Blockdevice:
all holders, subdevices and children of subdevices
"""
direct_children = [Blockdevice(child).name
direct_children = [
get_blockdevice(child, self.sysblock_dir, self.devnode_dir).name
for child in find_blockdevices(self.devdir)]
direct_children.extend(self.holders[:])
children = direct_children[:]
for dchild in direct_children:
children.extend(Blockdevice(dchild).children)
children.extend(get_blockdevice(dchild, self.sysblock_dir,
self.devnode_dir).children)
return children
def __get_device_nodes(self):
"""get all device nodes with the major/minor combination of the device
"""
result = []
major, minor = self.devnum
def find_major_minor(arg, dirname, fnames):
for fname in fnames:
try:
stat = os.stat(os.path.join(dirname, fname))
## check if it is a blockdevice and compare major/minor
if (stat.st_mode & 060000 == 060000) \
and (os.major(stat.st_rdev) == major) \
and (os.minor(stat.st_rdev) == minor):
result.append(os.path.join(dirname, fname))
except OSError:
pass
os.path.walk(self.devnode_dir, find_major_minor, None)
return result
def __str__(self):
"""display the name of the device
"""
return self.name
def info(self):
"""display some information about the device
"""
output = "%s:\n" % self.name
output += "\t%s:\t%s\n" % ("blockdir", self.devdir)
output += "\t%s:\t%s\n" % ("major/minor", self.devnum)
@ -187,12 +375,45 @@ class Blockdevice:
output += "\t%s:\t\t%s\n" % ("slaves", self.slaves)
output += "\t%s:\t%s\n" % ("holders", self.holders)
output += "\t%s:\t%s\n" % ("children", self.children)
output += "\t%s:\t%s\n" % ("device nodes", self.devnodes)
output += "\tflags:\t\t"
for funcname in [ "storage", "md_raid", "partitionable", "luks",
"lvm_pv", "lvm_lv"]:
if getattr(self, "is_%s" % funcname)():
output += "%s " % funcname
output += "\n"
return output
def get_blockdevice(dev,
sysblock_dir=DEFAULT_SYSBLOCK_DIR,
devnode_dir=DEFAULT_DEVNODE_DIR):
if os.path.isabs(dev):
if os.path.isfile(os.path.join(dev, "dev")):
devdir = dev
else:
return None
else:
for one_devdir in find_blockdevices(sysblock_dir):
if os.path.basename(one_devdir) == dev:
devdir = one_devdir
break
else:
return None
devname = os.path.basename(devdir)
dev = _get_cached_value(["blockdevices", devname])
if dev is None:
dev = Blockdevice(devdir, sysblock_dir, devnode_dir)
_set_cached_value(["blockdevices", devname], dev)
return dev
def find_blockdevices(top_dir):
cached = _get_cached_value(["blockdevice_dirs", top_dir])
if not cached is None:
return cached[:]
dev_dirs = []
def look4dev_dirs(arg, dirname, fnames):
@ -210,21 +431,141 @@ def find_blockdevices(top_dir):
fnames.remove(fname)
os.path.walk(top_dir, look4dev_dirs, 'dev')
return dev_dirs
_set_cached_value(["blockdevice_dirs", top_dir], dev_dirs)
return dev_dirs[:]
def find_lvm_pv():
"""return the blockdevice names of all physical LVM volumes
"""
cached = _get_cached_value(["lvm", "pv"])
if not cached is None:
return cached[:]
#TODO: should we check, if LVM is supported at all?
# e.g. by checking the existence of pvdisplay?
prefs = _load_preferences()
result = None
try:
proc = subprocess.Popen(
shell = False,
stdout = subprocess.PIPE,
args = [ prefs["Programs"]["super"],
prefs["Programs"]["CryptoBoxRootActions"],
"program", "pvdisplay" ])
proc.wait()
except OSError, err_msg:
# TODO: add a logging warning
result = []
if proc.returncode != 0:
# TODO: add a logging warning
result = []
if result is None:
result = []
for line in proc.stdout.readlines():
result.append(line.split(":", 1)[0].strip())
_set_cached_value(["lvm", "pv"], result)
return result[:]
def _get_cached_value(link):
"""return a cached value
"link" is an array of the hierachie of the accessed item
e.g. link = ["blockdevices", "hda"]
return None if the value is not in the cache or if USE_CACHE is False
"""
if not USE_CACHE:
return None
if "expires" in CACHED_VALUES:
if CACHED_VALUES["expires"] < int(time.time()):
reset_cache()
else:
__reset_cache_timer()
ref = CACHED_VALUES
for element in link:
if element in ref:
ref = ref[element]
else:
return None
return ref
def reset_cache():
## refresh the cache
for item in CACHED_VALUES:
CACHED_VALUES[item] = {}
__reset_cache_timer()
def __reset_cache_timer():
CACHED_VALUES["expires"] = int(time.time()) + CACHE_EXPIRE_SECONDS
def _set_cached_value(link, item):
"""store an item in the cache
"link" is an array of the hierachie of the accessed item
e.g. link = ["blockdevices", "hda"]
"""
if not USE_CACHE:
return
ref = CACHED_VALUES
for element in link[:-1]:
if not element in ref:
## create a non-existing sub element
ref[element] = {}
ref = ref[element]
## store the item
ref[link[-1]] = item
def _load_preferences():
prefs = cryptobox.core.settings.get_current_settings()
if not prefs is None:
## now the preferences are loaded
return prefs
## we have to load an emergency fallback for proper function
## this is mainly useful for local testing
root_dir = os.path.realpath(os.path.join(globals()["cryptobox"].__path__[0],
os.path.pardir, os.path.pardir))
config_file = os.path.join(root_dir, "bin", "cryptobox.conf")
## we have to chdir to the 'bin' directory - otherwise the paths in
## cryptobox.conf do not work
os.chdir(os.path.dirname(config_file))
return cryptobox.core.settings.CryptoBoxSettings(config_file)
if __name__ == '__main__':
blocks = Blockdevices()
for dev in blocks.get_devices():
print dev.info()
print
print "Usable storage devices:"
for dev in blocks.get_devices():
if dev.isstorage():
print dev
print
print "Partitionable devices:"
for dev in blocks.get_devices():
if dev.ispartitionable():
print dev
## list the properties of all available devices
## this is just for testing purposes
blocks = Blockdevices().get_devices()
## do we want to show the result?
def show(text=""):
if IS_VISIBLE:
print text
if len(blocks) > 0:
## show all devices and their properties
show("Properties of all devices:")
for device in blocks:
show(device.info())
## discover all self-check methods
example = blocks[0]
flag_checker = [ method for method in dir(example)
if callable(getattr(example, method))
and method.startswith("is_")]
## list all checks and the respective devices
for check in flag_checker:
show("List of '%s' devices:" % check[3:])
for device in blocks:
if getattr(device, check)():
show("\t%s" % device)
show()

View file

@ -275,7 +275,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"program", "cryptsetup",
"luksAddKey",
self.device,
"--batch-mode"])
@ -523,7 +523,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"program", "cryptsetup",
"luksOpen",
self.device,
self.name,
@ -542,7 +542,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
"program", "mount",
os.path.join(self.__dmDir, self.name),
self.__get_mount_point()])
proc.wait()
@ -572,7 +572,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"umount",
"program", "umount",
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
@ -588,7 +588,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"program", "cryptsetup",
"luksClose",
self.name,
"--batch-mode"])
@ -620,7 +620,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
"program", "mount",
self.device,
self.__get_mount_point()])
proc.wait()
@ -653,7 +653,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"umount",
"program", "umount",
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
@ -734,7 +734,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"program", "cryptsetup",
"luksFormat",
self.device,
"--batch-mode",
@ -755,7 +755,7 @@ class CryptoBoxContainer:
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"program", "cryptsetup",
"luksOpen",
self.device,
self.name,

View file

@ -40,6 +40,19 @@ VOLUMESDB_FILE = "cryptobox_volumes.db"
PLUGINCONF_FILE = "cryptobox_plugins.conf"
USERDB_FILE = "cryptobox_users.db"
## allow to retrieve the most recently created setting object
CURRENT_SETTING = []
def get_current_settings():
"""return the most recently created setting object
"""
if not CURRENT_SETTING:
return None
else:
return CURRENT_SETTING[0]
class CryptoBoxSettings:
"""Manage the various configuration files of the CryptoBox
@ -63,6 +76,7 @@ class CryptoBoxSettings:
self.misc_files = []
self.reload_misc_files()
self.__is_initialized = True
CURRENT_SETTING.insert(0, self)
def reload_misc_files(self):
@ -190,7 +204,7 @@ class CryptoBoxSettings:
args = [
self.prefs["Programs"]["super"],
self.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
"program", "mount",
"_tmpfs_",
mount_dir ])
(stdout, stderr) = proc.communicate()
@ -211,7 +225,7 @@ class CryptoBoxSettings:
args = [
self.prefs["Programs"]["super"],
self.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
"program", "mount",
partition,
mount_dir ])
(stdout, stderr) = proc.communicate()
@ -241,7 +255,7 @@ class CryptoBoxSettings:
args = [
self.prefs["Programs"]["super"],
self.prefs["Programs"]["CryptoBoxRootActions"],
"umount",
"program", "umount",
mount_dir ])
(stdout, stderr) = proc.communicate()
if proc.returncode != 0: