Browse Source

added minor parts of slovene translation

fixed samba integration script
added syslog support
replaced 'pattern' with 'level' for 'logs' plugin
improved fetch_po_files script
improved output of log plugin
recursively storing setting files fixed
umask set to 022
defining bootup and shutdown handler for the server
implementing volume_automount mounting
simplified the output preperation of the partition plugin
"busy" flag handling moved from core/container to core/main
master
lars 16 years ago
parent
commit
77324ae946
  1. 12
      README.samba
  2. 18
      bin/CryptoBoxWebserver
  3. 3
      bin/cryptobox-unittests.conf
  4. 1
      bin/cryptobox.conf
  5. 3
      bin/uml-setup.sh
  6. 2
      conf-examples/apache2_dav.conf
  7. 8
      conf-examples/cryptobox.conf
  8. 2
      conf-examples/samba-include.conf
  9. 10
      debian/changelog
  10. 2
      debian/control
  11. 2
      debian/cryptobox-server.postrm
  12. 13
      event-scripts/README
  13. 4
      event-scripts/apache2_dav
  14. 14
      event-scripts/samba
  15. 8
      intl/sl/cryptobox-server.po
  16. 25
      plugins/date/date.py
  17. 51
      plugins/date/intl/sl/cryptobox-server-feature-date.po
  18. 2
      plugins/date/root_action.py
  19. 2
      plugins/disks/unittests.py
  20. 2
      plugins/help/help.py
  21. 11
      plugins/help/intl/sl/cryptobox-server-feature-help.po
  22. 2
      plugins/help/unittests.py
  23. 13
      plugins/language_selection/intl/sl/cryptobox-server-feature-language_selection.po
  24. 2
      plugins/language_selection/unittests.py
  25. 23
      plugins/logs/language.hdf
  26. 17
      plugins/logs/logs.css
  27. 102
      plugins/logs/logs.py
  28. 58
      plugins/logs/show_log.cs
  29. 24
      plugins/logs/unittests.py
  30. 2
      plugins/network/network.py
  31. 2
      plugins/network/root_action.py
  32. 2
      plugins/network/unittests.py
  33. 10
      plugins/partition/language.hdf
  34. 92
      plugins/partition/partition.py
  35. 4
      plugins/plugin-interface.txt
  36. 6
      plugins/plugin_manager/plugin_manager.py
  37. 32
      plugins/plugin_manager/unittests.py
  38. 2
      plugins/system_preferences/unittests.py
  39. 2
      plugins/user_manager/user_manager.py
  40. 14
      plugins/volume_automount/unittests.py
  41. 22
      plugins/volume_automount/volume_automount.py
  42. 2
      plugins/volume_format_fs/language.hdf
  43. 8
      plugins/volume_mount/language.hdf
  44. 4
      plugins/volume_mount/unittests.py
  45. 80
      scripts/fetch_po_files.sh
  46. 2
      src/cryptobox/__init__.py
  47. 111
      src/cryptobox/core/container.py
  48. 71
      src/cryptobox/core/main.py
  49. 103
      src/cryptobox/core/settings.py
  50. 2
      src/cryptobox/core/tools.py
  51. 14
      src/cryptobox/plugins/base.py
  52. 2
      src/cryptobox/tests/test.cryptobox.py
  53. 25
      src/cryptobox/web/sites.py
  54. 8
      templates/language.hdf
  55. 8
      templates/macros.cs
  56. BIN
      www-data/dialog-error_tango.gif
  57. BIN
      www-data/dialog-error_tango.png
  58. BIN
      www-data/dialog-information_tango.gif
  59. BIN
      www-data/dialog-information_tango.png
  60. BIN
      www-data/dialog-warning_tango.gif
  61. BIN
      www-data/dialog-warning_tango.png

12
README.samba

@ -19,7 +19,6 @@ Reload the new samba configuration by calling:
invoke-rc.d samba reload
B) one share for each volume
Copy the example event script /usr/share/doc/cryptobox-server/event-script/samba
@ -27,14 +26,5 @@ to /etc/cryptobox-server/events.d/samba. This event handler will add and remove
shares whenever a volume is mounted or unmounted via the CryptoBox webinterface.
Add the following line to your /etc/samba/smb.conf:
include = /var/cache/cryptobox-server/samba-include.conf
Create this file:
touch /var/cache/cryptobox-server/samba-include.conf
Chown it to the cryptobox user:
chown cryptobox /var/cache/cryptobox-server/samba-include.conf
Reload the new samba configuration by calling:
invoke-rc.d samba reload
include = /var/cache/cryptobox-server/settings/misc/samba-include.conf

18
bin/CryptoBoxWebserver

@ -106,10 +106,24 @@ class CryptoBoxWebserver:
"staticFilter.file": os.path.realpath(os.path.join(opts.datadir, 'favicon.ico'))}
})
self.define_exit_handlers(cherrypy.root)
def define_exit_handlers(self, cbw):
import atexit
import signal
atexit.register(cbw.cleanup)
def kill_signal_handler(signum, frame):
cbw.cbox.log.info("Kill signal handler called: %d" % signum)
sys.exit(1)
signal.signal(signal.SIGTERM, kill_signal_handler)
def start(self):
cherrypy.server.start()
def fork_to_background():
## this is just copy'n'pasted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
@ -221,6 +235,8 @@ def parseOptions():
if __name__ == "__main__":
## process arguments
options = parseOptions()
## set umask to 022 (aka 755) - octal value
os.umask(022)
## run the webserver as a daemon process
if options.background: fork_to_background()
## write pid file

3
bin/cryptobox-unittests.conf

@ -76,7 +76,8 @@ Languages = en, de, sl, fr
[Programs]
cryptsetup = /sbin/cryptsetup
mkfs-data = /sbin/mkfs.ext3
mkfs = /sbin/mkfs
nice = /usr/bin/nice
blkid = /sbin/blkid
blockdev = /sbin/blockdev
mount = /bin/mount

1
bin/cryptobox.conf

@ -64,6 +64,7 @@ Destination = file
# syslog: $LOG_FACILITY
#Details = /var/log/cryptobox.log
Details = ./cryptobox.log
#Details = SYSLOG
[WebSettings]

3
bin/uml-setup.sh

@ -27,5 +27,6 @@ if [ ! -e "$TEST_IMG" ]
dd if=/dev/zero of="$TEST_IMG" bs=1M count=$TEST_SIZE
fi
linux ubd0="$ROOT_IMG" ubd1="$TEST_IMG" con=xterm hostfs=$PROJ_DIR fakehd eth0=daemon mem=$MEM_SIZE
# "aio=2.4" is necessary, as otherwise sfdiks hangs at "nanosleep({3,0})"
linux ubd0="$ROOT_IMG" ubd1="$TEST_IMG" con=xterm hostfs=$PROJ_DIR fakehd eth0=daemon mem=$MEM_SIZE aio=2.4

2
conf-examples/apache2_dav.conf

@ -5,7 +5,7 @@
<IfModule mod_dav_fs.c>
# include the dynamically managed configuration directory - IT MUST EXIST
Include /var/cache/cryptobox-server/settings/apache2_dav.conf.d/
Include /var/cache/cryptobox-server/settings/misc/apache2_dav.conf.d/
# lock database - should be writeable for www-data
DavLockDB /tmp/dav_lock.db
# a longer value than the default (120) help for high-latency networks

8
conf-examples/cryptobox.conf

@ -49,14 +49,14 @@ EventDir = /etc/cryptobox-server/events.d
Level = debug
# where to write the log messages to?
# possible values are: file
# syslog support will be added later
# possible values are 'file' and 'syslog'
Destination = file
# depending on the choosen destination (see above) you may select
# details. Possible values for the different destinations are:
# file: $FILENAME
# syslog: $LOG_FACILITY
# syslog: KERN | USER | MAIL | DAEMON | AUTH | SYSLOG | LPR | NEWS | UUCP
# | CRON | AUTHPRIV | LOCAL0 .. LOCAL7
Details = /var/log/cryptobox-server/cryptobox.log
@ -71,7 +71,7 @@ Stylesheet = /cryptobox-misc/cryptobox.css
# see /usr/share/locale for a list of possible language codes
# if a translated string is not available, then the english original is displayed
# available languages: cs, da, de, en, es, fi, fr, hu, it, ja, nl, pl, pt, ru, sl, sv
Languages = de, en, fr
Languages = de, en, fr, es, sl
[Programs]

2
conf-examples/samba-include.conf

@ -1,2 +0,0 @@
# DO NOT REMOVE OR EDIT THIS FILE
# the file was automatically generated by the cryptobox package

10
debian/changelog vendored

@ -1,3 +1,13 @@
cryptobox (0.2.54-1) unstable; urgency=low
* log plugin improved
* samba plugin fixed
* syslog support added
* improved output of 'logs' plugin
* finished 'volume_automount' plugin
-- Lars Kruse <devel@sumpfralle.de> Mon, 11 Dec 2006 11:52:38 +0100
cryptobox (0.2.53-1) unstable; urgency=low
* constant screen width

2
debian/control vendored

@ -9,7 +9,7 @@ Standards-Version: 3.7.2
Package: cryptobox-server
Architecture: any
Depends: ${python:Depends}, cryptsetup (>=20050111), e2fsprogs (>= 1.27), adduser, python (>=2.4), python-clearsilver, super, dosfstools, python-cherrypy, python-configobj
Suggests: samba, apache, stunnel
Suggests: samba, apache2, stunnel
Replaces: cryptobox
XB-Python-Version: ${python:Versions}
Description: Web interface for an encrypting fileserver

2
debian/cryptobox-server.postrm vendored

@ -15,7 +15,7 @@ remove_super_lines()
## do nothing, if there is no CryptoBox line
grep -q "CRYPTOBOX_MARKER" "$SUPER_FILE" || return 0
sed -i /CRYPTOBOX_MARKER/d "$SUPER_FILE"
sed -i /CryptoBoxRootActions/d "$SUPER_FILE"
sed -i /^CryptoBoxRootActions/d "$SUPER_FILE"
}

13
event-scripts/README

@ -2,12 +2,13 @@ Event scripts for CryptoBox events
If you want to execute specific actions according to changes of the cryptobox,
then you can just add your own scripts to this directory.
For every supported event of the CryptoBox, all scripts are called with root user
permissions.
These scripts are called with root user permissions.
The common synopsis for all event scripts is:
SCRIPTNAME EVENT [[EVENT_INFOS]...]
1) Possible events
Supported events:
premount|postmount|preumount|postumount:
called before and after (u)mounting of a volume
@ -18,9 +19,17 @@ Supported events:
- mount_dir: mountpoint of the volume
2) Preperation of event scripts
Every event script has to fulfill the following conditions:
- be executable (for the cryptobox user and for root)
- be writeable for root only
- its parent directories must be writeable for root only
- the directory of the script must contain a file called '_event_scripts_' (to prevent abuse)
3) Storing settings
If your custom event script needs to write information to a file, then it
should create this file below /var/cache/cryptobox-server/settings/misc/.
(adapt this directory to your setup, if you changed the default settings of
[Locations]->SettingsDir)

4
event-scripts/apache2_dav

@ -9,7 +9,7 @@
# (e.g. /etc/apache2/conf.d)
#
#
# Params: $event $volume_name $volume_type $mount_dir
# Params: $event $device $volume_name $volume_type $mount_dir
#
# event: premount | postmount | preumount | postumount
# device: name of the device
@ -24,7 +24,7 @@ set -eu
# adapt this part of the file to your setup
APACHE_SCRIPT=/etc/init.d/apache2
APACHE_CONF_DIR=/var/cache/cryptobox/apache2_dav.conf.d
APACHE_CONF_DIR=/var/cache/cryptobox/settings/misc/apache2_dav.conf.d
# this apache config snippet is used for every published volume
# _VOLUME_NAME_ and _SHARE_DIR_ are replaced by their actual values

14
event-scripts/samba

@ -7,10 +7,10 @@
# The following line _must_ be added to your /etc/samba/smb.conf:
# include = /var/cache/cryptobox-server/samba-include.conf
# and you should create this file and chown it to the cryptobox user:
# touch /var/cache/cryptobox-server/samba-include.conf
# touch /var/cache/cryptobox-server/settings/misc/samba-include.conf
#
#
# Params: $event $volume_name $volume_type $mount_dir
# Params: $event $device $volume_name $volume_type $mount_dir
#
# event: premount | postmount | preumount | postumount
# device: name of the device
@ -25,17 +25,15 @@ set -eu
# adapt this part of the file to your needs
SAMBA_CONTROL=smbcontrol
SAMBA_CONF_DIR=/var/cache/cryptobox-server/samba.conf.d
MAIN_SAMBA_CONF_FILE=/var/cache/cryptobox-server/samba-include.conf
SAMBA_CONF_DIR=/var/cache/cryptobox-server/settings/misc/samba.conf.d
MAIN_SAMBA_CONF_FILE=/var/cache/cryptobox-server/settings/misc/samba-include.conf
# this smb.conf snippet will get used for every published share
# _VOLUME_NAME and _SHARE_DIR_ are replaced by their actual values
# TODO: improve the later parsing of _SHARE_DIR_ in update_include_conf_file
# for now it depends on non existing whitespaces around the dirname
SAMBA_SHARE_TEMPLATE=$(cat - <<-"EOF"
[_VOLUME_NAME_]
comment = CryptoBox share
path =_SHARE_DIR_
path = _SHARE_DIR_
read only = no
guest ok = yes
EOF
@ -71,7 +69,7 @@ update_include_conf_file()
( echo "# this file was automatically generated by the CryptoBox"
echo "# DO NOT EDIT - all changes will get lost!"
find "$SAMBA_CONF_DIR" -type f -name "*.conf" | while read fname
do mdir=$(cat "$fname" | grep "path.*=" | cut -f 2 -d "=")
do mdir=$(grep "path.*=" "$fname" | cut -f 2 -d "=" | sed 's/^[ \t]*//')
# check if the mount directory still exists
if test -d "$mdir"
then echo "include = $fname"

8
intl/sl/cryptobox-server.po

@ -3,14 +3,14 @@ msgstr ""
"Project-Id-Version: CryptoBox-Server 0.3\n"
"Report-Msgid-Bugs-To: translate@cryptobox.org\n"
"POT-Creation-Date: 2006-11-28 05:03+0100\n"
"PO-Revision-Date: 2006-11-30 08:49+0100\n"
"PO-Revision-Date: 2006-12-11 01:40+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
"X-Generator: Translate Toolkit 0.10.1\n"
"X-Generator: Pootle 0.10.1\n"
#: Name
msgid "English"
@ -18,11 +18,11 @@ msgstr ""
#: Title.Top
msgid "The CryptoBox"
msgstr "Privatnost v vsako vas"
msgstr ""
#: Title.Slogan
msgid "Privacy for the rest of us."
msgstr ""
msgstr "Privatnost v vsako vas"
#: Title.Volume
msgid "Volume"

25
plugins/date/date.py

@ -47,22 +47,17 @@ class date(cryptobox.plugins.base.CryptoBoxPlugin):
datetime.datetime(year, month, day, hour, minute)
except ValueError:
self.hdf["Data.Warning"] = "Plugins.date.InvalidDate"
self.__prepare_form_data()
return "form_date"
date = "%02d%02d%02d%02d%d" % (month, day, hour, minute, year)
if self.__set_date(date):
self.cbox.log.info("changed date to: %s" % date)
self.hdf["Data.Success"] = "Plugins.date.DateChanged"
return "form_date"
else:
## a failure should usually be an invalid date (we do not check it really)
self.cbox.log.info("failed to set date: %s" % date)
self.hdf["Data.Warning"] = "Plugins.date.InvalidDate"
self.__prepare_form_data()
return "form_date"
else:
self.__prepare_form_data()
return "form_date"
date = "%02d%02d%02d%02d%d" % (month, day, hour, minute, year)
if self.__set_date(date):
self.cbox.log.info("changed date to: %s" % date)
self.hdf["Data.Success"] = "Plugins.date.DateChanged"
else:
## a failure should usually be an invalid date (we do not check it really)
self.cbox.log.info("failed to set date: %s" % date)
self.hdf["Data.Warning"] = "Plugins.date.InvalidDate"
self.__prepare_form_data()
return "form_date"
def get_status(self):

51
plugins/date/intl/sl/cryptobox-server-feature-date.po

@ -1,102 +1,101 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: CryptoBox-Server 0.3\n"
"Report-Msgid-Bugs-To: translate@cryptobox.org\n"
"POT-Creation-Date: 2006-11-28 05:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: 2006-12-09 17:00+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Translate Toolkit 0.10.1\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
"X-Generator: Pootle 0.10.1\n"
#: Name
msgid "Change date and time"
msgstr ""
msgstr "Spremeni datum in čas"
#: Link
msgid "Set date/time"
msgstr ""
msgstr "Nastavi datum in čas"
#: Title.ConfigDate
msgid "Date and time setting"
msgstr ""
msgstr "Nastavitve datum/čas"
#: Button.ConfigDate
msgid "Set date and time"
msgstr ""
msgstr "Nastavi datum/čas"
#: Text.Date
msgid "Date"
msgstr ""
msgstr "Datum"
#: Text.Time
msgid "Time"
msgstr ""
msgstr "Čas"
#: Text.Months.1
msgid "January"
msgstr ""
msgstr "Januar"
#: Text.Months.2
msgid "February"
msgstr ""
msgstr "Februar"
#: Text.Months.3
msgid "March"
msgstr ""
msgstr "Marec"
#: Text.Months.4
msgid "April"
msgstr ""
msgstr "April"
#: Text.Months.5
msgid "May"
msgstr ""
msgstr "Maj"
#: Text.Months.6
msgid "June"
msgstr ""
msgstr "Junij"
#: Text.Months.7
msgid "July"
msgstr ""
msgstr "Julij"
#: Text.Months.8
msgid "August"
msgstr ""
msgstr "Avgust"
#: Text.Months.9
msgid "September"
msgstr ""
msgstr "September"
#: Text.Months.10
msgid "October"
msgstr ""
msgstr "Oktober"
#: Text.Months.11
msgid "November"
msgstr ""
msgstr "November"
#: Text.Months.12
msgid "December"
msgstr ""
msgstr "December"
#: SuccessMessage.DateChanged.Title
msgid "Date changed"
msgstr ""
msgstr "Datum je spremenjen"
#: SuccessMessage.DateChanged.Text
msgid "The date was changed successfully."
msgstr ""
msgstr "Datum je bil uspešno spremenjen"
#: WarningMessage.InvalidDate.Title
msgid "Invalid value"
msgstr ""
msgstr "Neveljavna vrednost"
#: WarningMessage.InvalidDate.Text
msgid "An invalid value for date or time was supplied. Please try again."
msgstr ""
msgstr "Nepravilen vnos datuma ali časa. Prosimo poskusite ponovno."

2
plugins/date/root_action.py

@ -45,7 +45,7 @@ if __name__ == "__main__":
sys.stderr.write("%s: no argument supplied\n" % self_bin)
sys.exit(1)
if re.search(u'\D', args[0]):
if re.search(r'\D', args[0]):
sys.stderr.write("%s: illegal argument (%s)\n" % (self_bin, args[0]))
sys.exit(1)

2
plugins/disks/unittests.py

@ -36,7 +36,7 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
self.register_auth(self.url)
self.cmd.go(self.url + "disks?weblang=en")
self.cmd.find("Available disks")
self.cmd.find(u'Data.Status.Plugins.disks=(.*)$', "m")
self.cmd.find(r'Data.Status.Plugins.disks=(.*)$', "m")
devices = self.locals["__match__"].split(":")
self.assertTrue(len(devices)>0)
self.assertTrue("/dev/%s" % self.device in devices)

2
plugins/help/help.py

@ -43,7 +43,7 @@ class help(cryptobox.plugins.base.CryptoBoxPlugin):
import re, os
## check for invalid characters and if the page exists in the default language
if page and \
not re.search(u'\W', page) and \
not re.search(r'\W', page) and \
os.path.isfile(os.path.join(self.cbox.prefs["Locations"]["DocDir"],
self.default_lang, page + '.html')):
## everything is ok

11
plugins/help/intl/sl/cryptobox-server-feature-help.po

@ -1,22 +1,21 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: CryptoBox-Server 0.3\n"
"Report-Msgid-Bugs-To: translate@cryptobox.org\n"
"POT-Creation-Date: 2006-11-28 05:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: 2006-12-09 16:28+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Translate Toolkit 0.10.1\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
"X-Generator: Pootle 0.10.1\n"
#: Name
msgid "User manual"
msgstr ""
msgstr "Uporabniški priročnik"
#: Link
msgid "Help"
msgstr ""
msgstr "Pomoč"

2
plugins/help/unittests.py

@ -89,6 +89,6 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
def _getHelpStatus(self):
self.cmd.find(u'Data.Status.Plugins.help=(.*)$', "m")
self.cmd.find(r'Data.Status.Plugins.help=(.*)$', "m")
return tuple(self.locals["__match__"].split(":"))

13
plugins/language_selection/intl/sl/cryptobox-server-feature-language_selection.po

@ -1,26 +1,25 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: CryptoBox-Server 0.3\n"
"Report-Msgid-Bugs-To: translate@cryptobox.org\n"
"POT-Creation-Date: 2006-11-28 05:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: 2006-12-09 16:33+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Translate Toolkit 0.10.1\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
"X-Generator: Pootle 0.10.1\n"
#: Name
msgid "Choose interface language"
msgstr ""
msgstr "Izberite jezik"
#: Link
msgid "Languages"
msgstr ""
msgstr "Jezik"
#: Title.Language
msgid "Choose an interface language"
msgstr ""
msgstr "Izberite Jezik"

2
plugins/language_selection/unittests.py

@ -35,7 +35,7 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
url = self.url + "language_selection"
self.register_auth(url)
self.cmd.go(url)
self.cmd.find(u'Data.Status.Plugins.language_selection=(.*)$', "m")
self.cmd.find(r'Data.Status.Plugins.language_selection=(.*)$', "m")
langs = self.locals["__match__"].split(":")
self.assertTrue(len(langs)>1)
self.assertTrue(langs[0] == "en")

23
plugins/logs/language.hdf

@ -1,10 +1,23 @@
Name = Show the content of the log file
Link = Show log file
Name = Show event log
Link = Event log
Title.Log = CryptoBox logfiles
Title.Log = CryptoBox event log
Text {
EmptyLog = The logfile of the CryptoBox is empty.
Refresh = Refresh
ShowAll = Show all messages
AtLeastWarnings = Show warnings and errors
OnlyErrors = Show errors only
AgeOfEvent = Time passed
EventText = Description
TimeUnits {
Days = days
Hours = hours
Minutes = minutes
Seconds = seconds
}
}
AdviceMessage {
EmptyLog.Text = There are no messages available.
}

17
plugins/logs/logs.css

@ -1,6 +1,15 @@
#log p.console {
margin-left: 10%;
margin-right: 10%;
#log table.log td.level img {
width: 24px;
height: 24px;
vertical-align: middle;
}
#log table.log td.time {
padding: 0 5px 0 3px;
}
#log table.log td.text {
font-size: 0.8em;
font-family: monospace;
text-align: left;
}

102
plugins/logs/logs.py

@ -26,6 +26,14 @@ __revision__ = "$Id"
import cryptobox.plugins.base
import os
import re
import datetime
LOG_LEVELS = [ 'DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR' ]
LINE_REGEX = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}) " \
+ r"(?P<hour>\d{2}):(?P<minute>\d{2}):\d{2},\d{3} (?P<level>" \
+ "|".join([ "(?:%s)" % e for e in LOG_LEVELS]) + r"): (?P<text>.*)$")
class logs(cryptobox.plugins.base.CryptoBoxPlugin):
"""The logs feature of the CryptoBox.
@ -36,29 +44,31 @@ class logs(cryptobox.plugins.base.CryptoBoxPlugin):
request_auth = False
rank = 90
def do_action(self, lines=50, size=3000, pattern=None):
def do_action(self, lines=50, size=3000, level=None):
"""Show the latest part of the log file.
"""
import re
## filter input
try:
lines = int(lines)
if lines <= 0:
raise(ValueError)
except ValueError:
self.cbox.log.info("[logs] invalid line number: %s" % str(lines))
lines = 50
try:
size = int(size)
if size <= 0:
raise(ValueError)
except ValueError:
self.cbox.log.info("[logs] invalid log size: %s" % str(size))
size = 3000
if not pattern is None:
pattern = str(pattern)
if re.search(u'\W', pattern):
pattern = None
self.hdf[self.hdf_prefix + "Content"] = self.__get_log_content(
lines, size, pattern)
if not level is None:
level = str(level)
if not level in LOG_LEVELS:
self.cbox.log.info("[logs] invalid log level: %s" % str(level))
level = None
for (index, line) in enumerate(self.__get_log_content(lines, size, level)):
self.__set_line_hdf_data(self.hdf_prefix + "Content.%d" % index, line)
self.hdf[self.hdf_prefix + "StyleSheetFile"] = os.path.abspath(os.path.join(
self.plugin_dir, "logs.css"))
return "show_log"
@ -73,21 +83,79 @@ class logs(cryptobox.plugins.base.CryptoBoxPlugin):
self.cbox.prefs["Log"]["Details"])
def __get_log_content(self, lines, max_size, pattern):
def __get_log_content(self, lines, max_size, level):
"""Filter, sort and shorten the log content.
"""
if pattern:
if level and level in LOG_LEVELS:
filtered_levels = LOG_LEVELS[:]
## only the given and higher levels are accepted
while filtered_levels[0] != level:
del filtered_levels[0]
content = []
current_length = 0
for line in self.cbox.get_log_data():
if line.find(pattern) != -1:
content.append(line)
current_length += len(line)
if lines and len(content) >= lines:
break
if max_size and current_length >= max_size:
for one_level in filtered_levels:
if line.find(one_level) != -1:
break
else:
## the line does not contain an appropriate level name
continue
## we found a line that fits
content.append(line)
current_length += len(line)
if lines and len(content) >= lines:
break
if max_size and current_length >= max_size:
break
else:
content = self.cbox.get_log_data(lines, max_size)
return "<br/>".join(content)
return content
def __set_line_hdf_data(self, hdf_prefix, line):
"""Parse the log line for time and log level.
If parsing fails, then the output line is simply displayed without
meta information.
"""
self.hdf[hdf_prefix + ".Text"] = line.strip()
match = LINE_REGEX.match(line)
if not match:
## we could not parse the line - just return the text without meta info
return
## matching was successfully - we can parse the line for details
## calculate time difference of log line (aka: age of event)
try:
(year, month, day, hour, minute) = match.group(
'year', 'month', 'day', 'hour', 'minute')
(year, month, day, hour, minute) = \
(int(year), int(month), int(day), int(hour), int(minute))
## timediff is a timedelta object
timediff = datetime.datetime.today() - \
datetime.datetime(year, month, day, hour, minute)
## the time units (see below) correspond to the names within the language
## file: Text.TimeUnits.Days ...
if timediff.days >= 1:
self.hdf[hdf_prefix + ".TimeDiff.Unit"] = 'Days'
self.hdf[hdf_prefix + ".TimeDiff.Value"] = timediff.days
elif timediff.seconds >= 3600:
self.hdf[hdf_prefix + ".TimeDiff.Unit"] = 'Hours'
self.hdf[hdf_prefix + ".TimeDiff.Value"] = timediff.seconds / 3600
elif timediff.seconds >= 60:
self.hdf[hdf_prefix + ".TimeDiff.Unit"] = 'Minutes'
self.hdf[hdf_prefix + ".TimeDiff.Value"] = timediff.seconds / 60
else:
self.hdf[hdf_prefix + ".TimeDiff.Unit"] = 'Seconds'
self.hdf[hdf_prefix + ".TimeDiff.Value"] = timediff.seconds
except (OverflowError, TypeError, ValueError, IndexError), err_msg:
pass
## retrieve the level
try:
self.hdf[hdf_prefix + ".Level"] = match.group('level')
except IndexError:
pass
try:
self.hdf[hdf_prefix + ".Text"] = match.group('text').strip()
except IndexError:
pass

58
plugins/logs/show_log.cs

@ -6,21 +6,63 @@
<h1><?cs var:html_escape(Lang.Plugins.logs.Title.Log) ?></h1>
<div align="center">
<?cs call:print_form_header("log-form", "logs") ?>
<table border="0" align="center"><tr>
<td><?cs call:print_form_header("log-all", "logs") ?>
<button type="submit" value="refresh"><?cs
var:html_escape(Lang.Plugins.logs.Text.Refresh) ?></button>
</form>
</div>
var:html_escape(Lang.Plugins.logs.Text.ShowAll) ?></button>
</form></td>
<td><?cs call:print_form_header("log-warnings", "logs") ?>
<input type="hidden" name="level" value="WARNING" />
<button type="submit" value="refresh"><?cs
var:html_escape(Lang.Plugins.logs.Text.AtLeastWarnings) ?></button>
</form></td>
<td><?cs call:print_form_header("log-only-error", "logs") ?>
<input type="hidden" name="level" value="ERROR" />
<button type="submit" value="refresh"><?cs
var:html_escape(Lang.Plugins.logs.Text.OnlyErrors) ?></button>
</form></td>
</tr></table>
<?cs call:handle_messages() ?>
<div id="log">
<?cs if:Data.Plugins.logs.Content ?>
<p class="console"><?cs var:Data.Plugins.logs.Content ?></p>
<?cs if:subcount(Data.Plugins.logs.Content) > 0 ?>
<table class="log"><?cs # the first line dictates if we show meta info or not
?><?cs if:Data.Plugins.logs.Content.0.Level ?>
<tr><th></th>
<th><?cs var:html_escape(Lang.Plugins.logs.Text.AgeOfEvent) ?></th>
<th><?cs var:html_escape(Lang.Plugins.logs.Text.EventText) ?></th>
</tr>
<?cs loop:index = #0, subcount(Data.Plugins.logs.Content)-1, #1 ?><?cs
with:x=Data.Plugins.logs.Content[index] ?><?cs
if:x.Text ?><?cs
if:x.Level == "ERROR" ?><?cs
set:meta_file="dialog-error_tango.gif" ?><?cs
elif:x.Level == "WARNING" ?><?cs
set:meta_file="dialog-warning_tango.gif" ?><?cs
else ?><?cs
set:meta_file="dialog-information_tango.gif" ?><?cs
/if ?>
<tr><td class="level"><img src="<?cs
call:link("cryptobox-misc/" + meta_file, "", "", "", "") ?>"
alt="symbol: <?cs var:html_escape(x.Level) ?>" /></td>
<td class="time"><?cs if:x.TimeDiff.Value ?><?cs
var:html_escape(x.TimeDiff.Value) ?>&nbsp;<?cs
var:html_escape(Lang.Plugins.logs.Text.TimeUnits[
x.TimeDiff.Unit]) ?><?cs
/if ?></td>
<td class="text"><?cs var:html_escape(x.Text) ?></td></tr><?cs
/if ?><?cs /with ?><?cs /loop ?><?cs
else ?><?cs
loop:index = #0, subcount(Data.Plugins.logs.Content)-1, #1 ?><?cs
with:x=Data.Plugins.logs.Content[index] ?>
<tr><td><?cs var:html_escape(x.Text) ?></td></tr><?cs
/with ?><?cs /loop ?><?cs
/if ?>
</table>
<?cs else ?>
<p><?cs var:html_escape(Lang.Plugins.logs.Text.EmptyLog) ?></p>
<?cs call:hint("Plugins.logs.EmptyLog") ?>
<?cs /if ?>
</div>

24
plugins/logs/unittests.py

@ -28,32 +28,32 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
log_url = self.url + "logs"
self.register_auth(log_url)
self.cmd.go(log_url)
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
def test_write_logs(self):
log_text = "unittest - just a marker - please ignore"
self.cbox.log.error(log_text)
log_url = self.url + "logs"
self.register_auth(log_url)
self.cmd.go(log_url + "?pattern=ERROR")
self.cmd.go(log_url + "?level=ERROR")
self.cmd.find(log_text)
def test_invalid_args(self):
log_url = self.url + "logs"
self.cmd.go(log_url + "?lines=10")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?lines=0")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?lines=x")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?size=1000")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?size=0")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?size=x")
self.cmd.find('class="console"')
self.cmd.go(log_url + "?pattern=foobar")
self.cmd.find('class="console"')
self.cmd.go(log_url + u"?pattern=kfj!^(]")
self.cmd.find('class="console"')
self.cmd.find('<table class="log">')
self.cmd.go(log_url + "?level=foobar")
self.cmd.find('<table class="log">')
self.cmd.go(log_url + r"?level=kfj!^(]")
self.cmd.find('<table class="log">')

2
plugins/network/network.py

@ -128,7 +128,7 @@ class network(cryptobox.plugins.base.CryptoBoxPlugin):
if proc.returncode != 0:
return (0,0,0,0)
## this regex matches the four numbers of the IP
match = re.search(u'inet [\w]+:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\s', stdout)
match = re.search(r'inet [\w]+:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\s', stdout)
if match:
## use the previously matched numbers
return tuple([int(e) for e in match.groups()])

2
plugins/network/root_action.py

@ -50,7 +50,7 @@ if __name__ == "__main__":
sys.stderr.write("%s: no argument supplied\n" % self_bin)
sys.exit(1)
match = re.search(u'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$', args[0])
match = re.search(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$', args[0])
## did we match? If yes, then: are there wrong values inside?
if not match or [e for e in match.groups() if int(e) > 255]:
sys.stderr.write("%s: illegal argument (%s)\n" % (self_bin, args[0]))

2
plugins/network/unittests.py

@ -38,7 +38,7 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
def get_current_ip():
self.register_auth(self.url + "network")
self.cmd.go(self.url + "network")
self.cmd.find(u'Data.Status.Plugins.network=([0-9\.]*)$', "m")
self.cmd.find(r'Data.Status.Plugins.network=([0-9\.]*)$', "m")
return self.locals["__match__"]
orig_ip_text = get_current_ip()
orig_ip_octs = orig_ip_text.split(".")

10
plugins/partition/language.hdf

@ -57,16 +57,18 @@ WarningMessage {
PartitioningFailed {
Title = Partitioning failed
Text = The partitioning of the device failed for some reason - sorry!
Link.Text = Show log messages
Link.Attr1.name = pattern
Link.Text = Show log messages
Link.Rel = logs
Link.Attr1.name = level
Link.Attr1.value = ERROR
}
FormattingFailed {
Title = Formatting failed
Text = The formatting of the filesystems of the device failed - sorry!
Text = Formatting of at least one volume failed - sorry!
Link.Text = Show log messages
Link.Attr1.name = pattern
Link.Rel = logs
Link.Attr1.name = level
Link.Attr1.value = ERROR
}

92
plugins/partition/partition.py

@ -209,37 +209,34 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
## partition is still part of the containerlist, as the label is not
## checked again - very ugly!!! So we will call reReadContainerList
## after formatting the last partition - see below
self.cbox.reread_container_list()
def result_generator():
"""Generate the results of formatting - may be threaded.
"""
counter = 0
## initialize the generator
format_part_gen = self.__format_partitions(parts)
while counter < len(parts):
## first part: get the device name
yield format_part_gen.next()
counter += 1
## second part: do the real formatting of a partition
result = format_part_gen.next()
## after the first partiton, we can reRead the containerList
## (as the possible config partition was already created)
if self.with_config_partition and (counter == 1):
## important: reRead the containerList - but somehow it
## breaks the flow (hanging process)
#self.cbox.reReadContainerList()
## write config data
self.cbox.prefs.mount_partition()
self.cbox.prefs.write()
self.cbox.log.info("settings stored on config partition")
## return the result
if result:
yield "OK"
else:
yield "<b>Error</b>"
return {
"template": "show_format_progress",
"generator": result_generator}
#self.cbox.reread_container_list()
format_ok = True
counter = 0
## initialize the generator
format_part_gen = self.__format_partitions(parts)
while counter < len(parts):
## first part: get the device name
counter += 1
## second part: do the real formatting of a partition
result = format_part_gen.next()
## after the first partiton, we can reRead the containerList
## (as the possible config partition was already created)
if self.with_config_partition and (counter == 1):
## important: reRead the containerList - but somehow it
## breaks the flow (hanging process)
#self.cbox.reReadContainerList()
## write config data
self.cbox.prefs.mount_partition()
self.cbox.prefs.write()
self.cbox.log.info("settings stored on config partition")
## return the result
if not result:
format_ok = False
if format_ok:
self.hdf["Data.Success"] = "Plugins.partition.Partitioned"
else:
self.hdf["Data.Warning"] = "Plugins.partition.FormattingFailed"
return "empty"
else:
return self.__action_add_partition(args)
@ -382,6 +379,9 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
self.device])
for line in self.__get_sfdisk_layout(parts, is_filled):
proc.stdin.write(line + "\n")
#TODO: if running inside of an uml, then sfdisk hangs at "nanosleep({3,0})"
# very ugly - maybe a uml bug?
# it seems, like this can be avoided by running uml with the param "aio=2.4"
(output, error) = proc.communicate()
if proc.returncode != 0:
self.cbox.log.debug("partitioning failed: %s" % error)
@ -439,7 +439,6 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
dev_name = self.device + str(part_num)
part_type = PARTTYPES[parts[0]["type"]][1]
self.cbox.log.info("formatting partition (%s) as '%s'" % (dev_name, part_type))
yield dev_name
yield self.__format_one_partition(dev_name, part_type)
del parts[0]
## other data partitions
@ -449,7 +448,6 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
part_type = PARTTYPES[parts[0]["type"]][1]
self.cbox.log.info("formatting partition (%s) as '%s'" % \
(dev_name, part_type))
yield dev_name
yield self.__format_one_partition(dev_name, part_type)
part_num += 1
del parts[0]
@ -476,34 +474,6 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
return True
def __old_format_one_partition(self, dev_name, fs_type):
"""Format a single partition
"""
## first: retrieve UUID - it can be removed from the database afterwards
prev_name = [e.get_name() for e in self.cbox.get_container_list()
if e.get_device() == dev_name]
## call "mkfs"
proc = subprocess.Popen(
shell = False,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"plugin",
os.path.join(self.plugin_dir, "root_action.py"),
"format",
dev_name,
fs_type])
(output, error) = proc.communicate()
if proc.returncode != 0:
self.cbox.log.warn("failed to create filesystem on %s: %s" % (dev_name, error))
return False
else:
## remove unused volume entry
if prev_name:
del self.cbox.prefs.volumes_db[prev_name[0]]
return True
def __set_label_of_partition(self, dev_name, label):
"""Set the label of a partition - useful for the config partition.
"""

4
plugins/plugin-interface.txt

@ -30,6 +30,10 @@ Python code interface:
- function "get_status":
- returns a string, that describes a state connected to this plugin (e.g. the current date and
time (for the "date" plugin))
- function "setup":
- may be overridden to specify bootup behaviour
- function "cleanup":
- may be overridden to specify shutdown behaviour
- the class variable "plugin_capabilities" must be an array of strings (supported: "system" and
"volume")
- the class variable "plugin_visibility" may contain one or more of the following items:

6
plugins/plugin_manager/plugin_manager.py

@ -35,7 +35,7 @@ class plugin_manager(cryptobox.plugins.base.CryptoBoxPlugin):
import re
if plugin_name:
## check for invalid characters
if re.search(u'\W', plugin_name): return "plugin_list"
if re.search(r'\W', plugin_name): return "plugin_list"
plugin_manager = cryptobox.plugins.manage.PluginManager(
self.cbox, self.cbox.prefs["Locations"]["PluginDir"])
plugin = plugin_manager.get_plugin(plugin_name)
@ -51,7 +51,7 @@ class plugin_manager(cryptobox.plugins.base.CryptoBoxPlugin):
elif store:
for key in args.keys():
if key.endswith("_listed"):
if not re.search(u'\W',key):
if not re.search(r'\W',key):
self.__setConfig(key[:-7], args)
else:
self.cbox.log.info("plugin_manager: invalid plugin name (%s)" % \
@ -129,7 +129,7 @@ class plugin_manager(cryptobox.plugins.base.CryptoBoxPlugin):
setting = {}
setting["visibility"] = []
## look for "_visible_" values and apply them
pattern = re.compile(u'%s_visible_([\w]+)$' % name)
pattern = re.compile(r'%s_visible_([\w]+)$' % name)
for key in args.keys():
if key.startswith(name + "_visible_"):
(vis_type, ) = pattern.match(key).groups()

32
plugins/plugin_manager/unittests.py

@ -34,25 +34,25 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
def test_set_options(self):
url = self.url + "plugin_manager"
self.register_auth(url)
self.cmd.go(url + u"?plugin_name=t/-!")
self.cmd.go(url + r"?plugin_name=t/-!")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?plugin_name=foobar")
self.cmd.go(url + r"?plugin_name=foobar")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?plugin_name=disks&action=up")
self.cmd.go(url + r"?plugin_name=disks&action=up")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?plugin_name=disks&action=down")
self.cmd.go(url + r"?plugin_name=disks&action=down")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&dis/ks_listed")
self.cmd.go(url + r"?store=1&dis/ks_listed")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_rank=50")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_rank=50")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_rank=x")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_rank=x")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_auth=1")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_auth=1")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_rank=50&disks_auth=1")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_rank=50&disks_auth=1")
self.cmd.find('Plugin Manager')
@ -60,11 +60,11 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
#TODO: if we want to be perfect, then we should check the change of the rank
url = self.url + "plugin_manager"
self.register_auth(url)
self.cmd.go(url + u"?plugin_name=disks&action=up")
self.cmd.go(url + r"?plugin_name=disks&action=up")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_rank=0")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_rank=0")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?plugin_name=disks&action=up")
self.cmd.go(url + r"?plugin_name=disks&action=up")
self.cmd.find('Plugin Manager')
@ -72,11 +72,11 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
## TODO: if we want to be perfect, then we should check the change of the rank
url = self.url + "plugin_manager"
self.register_auth(url)
self.cmd.go(url + u"?plugin_name=disks&action=down")
self.cmd.go(url + r"?plugin_name=disks&action=down")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?store=1&disks_listed&disks_visible_menu=1&disks_rank=100")
self.cmd.go(url + r"?store=1&disks_listed&disks_visible_menu=1&disks_rank=100")
self.cmd.find('Plugin Manager')
self.cmd.go(url + u"?plugin_name=disks&action=down")
self.cmd.go(url + r"?plugin_name=disks&action=down")
self.cmd.find('Plugin Manager')

2
plugins/system_preferences/unittests.py

@ -31,7 +31,7 @@ class unittests(cryptobox.web.testclass.WebInterfaceTestClass):
def test_check_plugins(self):
self.cmd.go(self.url + "system_preferences")
self.cmd.find(u'Data.Status.Plugins.system_preferences=(.*)$', "m")
self.cmd.find(r'Data.Status.Plugins.system_preferences=(.*)$', "m")
plugins = self.locals["__match__"].split(":")
self.assertTrue(len(plugins) > 1)
self.assertTrue("disks" in plugins)

2
plugins/user_manager/user_manager.py