Browse Source

adapt partition plugin to the new blockdevice implementation

reduce minimum storage block size from 20 to 10 (the config partition is small)
master
lars 14 years ago
parent
commit
9c649312d0
  1. 133
      plugins/partition/partition.py
  2. 56
      src/cryptobox/core/blockdevice.py
  3. 5
      src/cryptobox/core/main.py
  4. 248
      src/cryptobox/core/tools.py
  5. 2
      src/cryptobox/web/sites.py

133
plugins/partition/partition.py

@ -27,7 +27,7 @@ import subprocess
import os
import re
import logging
import cryptobox.core.tools as cbox_tools
import cryptobox.core.blockdevice as blockdevice_tools
import cryptobox.plugins.base
from cryptobox.core.exceptions import *
@ -58,15 +58,19 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
self.__prepare_dataset()
## retrieve some values from 'args' - defaults are empty
self.blockdevice = self.__get_selected_device(args)
self.with_config_partition = self.__is_with_config_partition()
if self.blockdevice:
self.with_config_partition = self.__is_with_config_partition()
self.blockdevice_size = self.__get_available_device_size(self.blockdevice)
else:
self.with_config_partition = False
self.blockdevice_size = 0
self.cbox.log.debug(
"partition plugin: selected device=%s" % str(self.blockdevice))
self.blockdevice_size = self.__get_available_device_size(self.blockdevice)
## no (or invalid) device was supplied
if not self.blockdevice:
return self.__action_select_device()
## exit if the blockdevice is not writeable
if not os.access(self.blockdevice, os.W_OK):
if not os.access(self.blockdevice.devnodes[0], os.W_OK):
self.hdf["Data.Warning"] = "DeviceNotWriteable"
return self.__action_select_device()
## no confirm setting?
@ -91,15 +95,19 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
"partition: invalid partition number to delete (%s)" % del_args[0])
return self.__action_select_device()
return self.__action_del_partition(args, num_part)
else: # for "select_device" and for invalid targets
else:
## for "select_device" and for invalid targets
return self.__action_select_device()
def get_status(self):
"""The status of this plugin is the selected device and some information.
"""
return "%s / %s / %s" % (self.blockdevice, self.blockdevice_size,
self.with_config_partition)
if not self.blockdevice:
return "no blockdevice selected"
else:
return "%s / %s / %s" % (self.blockdevice.name,
self.blockdevice_size, self.with_config_partition)
def get_warnings(self):
@ -131,7 +139,13 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
"""Check the selected device (valid, not busy, ...).
"""
try:
blockdevice = args["block_device"]
blockdevice_name = args["block_device"]
found = [ dev for dev in
blockdevice_tools.Blockdevices().get_partitionable_devices()
if dev.name == blockdevice_name ]
if not found:
return None
blockdevice = found[0]
except KeyError:
return None
if not self.__is_device_valid(blockdevice):
@ -149,8 +163,6 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
return False
if not self.cbox.is_device_allowed(blockdevice):
return False
if not blockdevice in cbox_tools.get_parent_blockdevices():
return False
return True
@ -158,9 +170,10 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
"""check if the device (or one of its partitions) is mounted
"""
## the config partition is ignored, as it will get unmounted if necessary
for cont in self.cbox.get_container_list():
if cbox_tools.is_part_of_blockdevice(blockdevice, cont.get_device()) \
and cont.is_mounted():
for dev in blockdevice.children:
container = self.cbox.get_container(
blockdevice_tools.get_blockdevice(dev).devnodes[0])
if container and (container.is_mounted() or container.is_busy()):
return True
return False
@ -168,15 +181,17 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
def __action_select_device(self):
"""Show a form to select the device for partitioning.
"""
block_devices = [e
for e in cbox_tools.get_parent_blockdevices()
if self.cbox.is_device_allowed(e)]
block_devices = [ e for e in
blockdevice_tools.Blockdevices().get_partitionable_devices()
if self.cbox.is_device_allowed(e) ]
counter = 0
for dev in block_devices:
self.hdf[self.hdf_prefix + "BlockDevices.%d.name" % counter] = dev
self.hdf[self.hdf_prefix + "BlockDevices.%d.name" % counter] = \
dev.name
self.hdf[self.hdf_prefix + "BlockDevices.%d.size" % counter] = \
cbox_tools.get_blockdevice_size_humanly(dev)
self.cbox.log.debug("found a suitable block device: %s" % dev)
dev.size_human
self.cbox.log.debug("found a suitable block device: %s" % \
dev.devnodes[0])
counter += 1
if self.with_config_partition:
self.hdf[self.hdf_prefix + "CreateConfigPartition"] = "1"
@ -189,7 +204,7 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
def __action_add_partition(self, args):
"""Add a selected partition to the currently proposed partition table.
"""
self.hdf[self.hdf_prefix + "Device"] = self.blockdevice
self.hdf[self.hdf_prefix + "Device"] = self.blockdevice.name
self.hdf[self.hdf_prefix + "Device.Size"] = self.blockdevice_size
parts = self.__get_partitions_from_args(args)
self.__set_partition_data(parts)
@ -199,7 +214,7 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
def __action_del_partition(self, args, part_num):
"""Remove a partition from the proposed partition table.
"""
self.hdf[self.hdf_prefix + "Device"] = self.blockdevice
self.hdf[self.hdf_prefix + "Device"] = self.blockdevice.name
self.hdf[self.hdf_prefix + "Device.Size"] = self.blockdevice_size
parts = self.__get_partitions_from_args(args)
## valid partition number to be deleted?
@ -215,8 +230,11 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
parts = self.__get_partitions_from_args(args)
if parts:
self.__set_partition_data(parts)
if cbox_tools.is_part_of_blockdevice(self.blockdevice,
self.cbox.prefs.get_active_partition()):
## umount config partition if necessary
config_partition = self.cbox.prefs.get_active_partition()
if [ dev for dev in self.blockdevice.children
if config_partition in
blockdevice_tools.get_blockdevice(dev).devnodes ]:
self.cbox.prefs.umount_partition()
if not self.__run_fdisk(parts):
self.hdf["Data.Warning"] = "Plugins.partition.PartitioningFailed"
@ -257,9 +275,10 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
format_ok = False
if format_ok:
self.hdf["Data.Success"] = "Plugins.partition.Partitioned"
return { "plugin":"system_preferences", "values":[] }
else:
self.hdf["Data.Warning"] = "Plugins.partition.FormattingFailed"
return "empty"
return "empty"
else:
return self.__action_add_partition(args)
@ -270,15 +289,17 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
import types
## we do not have to take special care for a possible config partition
parts = [ { "size": self.blockdevice_size, "type": "windows" } ]
## umount partition if necessary
if cbox_tools.is_part_of_blockdevice(self.blockdevice,
self.cbox.prefs.get_active_partition()):
## umount config partition if necessary
config_partition = self.cbox.prefs.get_active_partition()
if [ dev for dev in self.blockdevice.children
if config_partition in
blockdevice_tools.get_blockdevice(dev).devnodes ]:
self.cbox.prefs.umount_partition()
## partition it
if not self.__run_fdisk(parts):
self.hdf["Data.Warning"] = "Plugins.partition.PartitioningFailed"
return None
## "formatPartitions" is a generator, returning device names and bolean values
## "formatPartitions" is a generator, returning device names and boolean values
result = [e for e in self.__format_partitions(parts)
if type(e) == types.BooleanType]
if self.with_config_partition:
@ -317,11 +338,12 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
for ptype in PARTTYPES.keys():
self.hdf[self.hdf_prefix + "Types.%s" % ptype] = ptype
## store the currently existing partitions of the choosen block device
current_containers = [ e for e in self.cbox.get_container_list()
if cbox_tools.is_part_of_blockdevice(self.blockdevice, e.get_device()) ]
current_containers = [ blockdevice_tools.get_blockdevice(dev)
for dev in self.blockdevice.children
if blockdevice_tools.get_blockdevice(dev).is_storage() ]
for (index, cont) in enumerate(current_containers):
self.hdf[self.hdf_prefix + "ExistingContainers.%d" % index] = \
cont.get_device()
cont.name
def __get_partitions_from_args(self, args):
@ -361,11 +383,11 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
"""calculate the available size (MB) of the device
also consider a (possible) configuration partition
"""
device_size = cbox_tools.get_blockdevice_size(device)
if device_size < 0:
return 0
device_size = device.size
if self.with_config_partition:
device_size -= CONFIGPARTITION["size"]
if device_size < 0:
return 0
return device_size
@ -378,7 +400,12 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
if not active:
return True
## check if the active one is part of the current device
return cbox_tools.is_part_of_blockdevice(self.blockdevice, active)
if [ dev for dev in self.blockdevice.children
if active in
blockdevice_tools.get_blockdevice(dev).devnodes ]:
return True
else:
return False
return False
@ -402,7 +429,7 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
"plugin",
os.path.join(self.plugin_dir, "root_action.py"),
"partition",
self.blockdevice])
self.blockdevice.devnodes[0]])
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})"
@ -455,22 +482,22 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
part_num = 1
## maybe a config partition?
if self.with_config_partition:
dev_name = self.__get_partition_name(self.blockdevice, part_num)
dev_name = self.__get_partition_device(self.blockdevice, part_num)
self.cbox.log.info("formatting config partition (%s)" % dev_name)
if self.__format_one_partition(dev_name, CONFIGPARTITION["fs"]):
self.__set_label_of_partition(dev_name,
self.cbox.prefs["Main"]["ConfigVolumeLabel"])
part_num += 1
## the first data partition
dev_name = self.__get_partition_name(self.blockdevice, part_num)
dev_name = self.__get_partition_device(self.blockdevice, part_num)
part_type = PARTTYPES[parts[0]["type"]][1]
self.cbox.log.info("formatting partition (%s) as '%s'" % (dev_name, part_type))
yield self.__format_one_partition(dev_name, part_type)
del parts[0]
## other data partitions
part_num = 5
part_num += 1
while parts:
dev_name = self.__get_partition_name(self.blockdevice, part_num)
dev_name = self.__get_partition_device(self.blockdevice, part_num)
part_type = PARTTYPES[parts[0]["type"]][1]
self.cbox.log.info("formatting partition (%s) as '%s'" % \
(dev_name, part_type))
@ -480,17 +507,26 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
return
def __get_partition_name(self, blockdev, number):
def __get_partition_device(self, blockdev, number):
"""Return the devicename of a specific partition of a device
No tests are performed, whether the partition exists or
not.
"""
if re.search("[0-9]$", blockdev):
## blockdev ends with a digit, so it is a partition, we insert a 'p'
return "%sp%d" % (blockdev, number)
else:
## whole disk, no 'p' necessary
return "%s%d" % (blockdev, number)
valid_children = []
## filter the storage devices
for child in blockdev.children:
childdev = blockdevice_tools.get_blockdevice(child)
if childdev and childdev.is_storage():
valid_children.append(childdev)
sorted = blockdevice_tools.get_sorted_devices(valid_children)
if number <= len(sorted):
childdev = sorted[number-1]
if childdev:
return childdev.devnodes[0]
self.cbox.log.warn("Failed to get the partition name (%s, %d)" % \
(blockdev, number))
## return some guessed value - we should never get here ...
return "%s%d" % (blockdev.devnodes[0], number)
def __format_one_partition(self, dev_name, fs_type):
@ -502,7 +538,8 @@ class partition(cryptobox.plugins.base.CryptoBoxPlugin):
if e.get_device() == dev_name]
## call "mkfs"
try:
cont = cryptobox.core.container.CryptoBoxContainer(dev_name, self.cbox)
format_dev = blockdevice_tools.get_blockdevice(dev_name)
cont = cryptobox.core.container.CryptoBoxContainer(format_dev, self.cbox)
cont.create(cryptobox.core.container.CONTAINERTYPES["plain"], fs_type=fs_type)
except (CBInvalidType, CBCreateError, CBVolumeIsActive), err_msg:
self.cbox.log.warn(err_msg)

56
src/cryptobox/core/blockdevice.py

@ -38,7 +38,7 @@ LOGGER = logging.getLogger("CryptoBox")
DEFAULT_SYSBLOCK_DIR = '/sys/block'
DEFAULT_DEVNODE_DIR = '/dev'
MINIMUM_STORAGE_SIZE = 20
MINIMUM_STORAGE_SIZE = 10
MAJOR_DEVNUM_RAM = 1
MAJOR_DEVNUM_LOOP = 7
MAJOR_DEVNUM_MD_RAID = 9
@ -98,6 +98,13 @@ class Blockdevice:
sysblock_dir=DEFAULT_SYSBLOCK_DIR,
devnode_dir=DEFAULT_DEVNODE_DIR):
"""initialize the blockdevice
@type dev: string
@param dev: The /sys/block/ subdirectory describing a blockdevice
@type sysblock_dir: string
@param sysblock_dir: The linux /sys/ directory. Default is '/sys'.
@type devnode_dir: string
@param devnode_dir: The linux /dev/ directory. Default is '/dev'.
"""
self.devdir = dev
self.devnode_dir = devnode_dir
@ -122,6 +129,9 @@ class Blockdevice:
usually we will have to reset the cache, too
just in case of first-time initialization, this is not necessary
@type empty_cache: boolean
@param empty_cache: Whether to discard the cached information or not.
"""
CACHE.reset(["blockdevice_info", self.name])
self.devnum = self.__get_major_minor()
@ -141,6 +151,9 @@ class Blockdevice:
"""check if the device is usable and valid
causes of invalidity: ram device, loop device, removable device
@rtype: boolean
@return: 'True' for a valid blockdevice
"""
if not self.devnodes:
return False
@ -165,6 +178,9 @@ class Blockdevice:
def is_storage(self):
"""return if this device can be used as a storage
@rtype: boolean
@return: 'True' for a device usable as a storage
"""
## check the cache first
cache_link = ["blockdevice_info", self.name, "is_storage"]
@ -755,11 +771,24 @@ 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
## it is an absolute path
if dev.startswith(devnode_dir):
## it is the name of a devicenode (e.g.: '/dev/hda1')
found_dev = [ a_dev for a_dev in Blockdevices(
sysblock_dir, devnode_dir).get_devices()
if dev in a_dev.devnodes ]
if found_dev:
devdir = found_dev[0].devdir
else:
return None
else:
return None
## it is the path of a /sys/ subdirectory (e.g.: '/sys/block/hda')
if os.path.isfile(os.path.join(dev, "dev")):
devdir = dev
else:
return None
else:
## the name of a blockdevice (e.g.: 'dm-0')
for one_devdir in find_blockdevices(sysblock_dir):
if os.path.basename(one_devdir) == dev:
devdir = one_devdir
@ -836,6 +865,25 @@ def find_lvm_pv():
return result[:]
def get_sorted_devices(names):
"""return the names of devices in a sorted order
e.g.: "hda1", "hda5", "hda6", ..., "hda10", "hda11"
"""
# TODO: implement this for devicenames like "hda12"
def compare_device_names(x, y):
if x.name < y.name:
return -1
elif x.name == y.name:
return 0
else:
return 1
result = names[:]
result.sort(cmp=compare_device_names)
return result
def _load_preferences():
prefs = cryptobox.core.settings.get_current_settings()
if not prefs is None:

5
src/cryptobox/core/main.py

@ -213,6 +213,11 @@ class CryptoBox:
also check, if the device is readable and writeable for the current user
"""
import types
## if "device" is a string, then turn it into a blockdevice object
if type(device) == types.StringType:
device = blockdevice.get_blockdevice(device)
if device is None:
return False
allowed = self.prefs["Main"]["AllowedDevices"]
if type(allowed) == types.StringType:
allowed = [allowed]

248
src/cryptobox/core/tools.py

@ -1,248 +0,0 @@
#
# Copyright 2006 sense.lab e.V.
#
# This file is part of the CryptoBox.
#
# The CryptoBox 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 2 of the License, or
# (at your option) any later version.
#
# The CryptoBox 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 the CryptoBox; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""Some useful functions for the CryptoBox.
"""
__revision__ = "$Id"
import logging
import os
import re
LOGGER = logging.getLogger("CryptoBox")
def get_available_partitions():
"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
## ignore lines with: invalid minor/major or extend partitions (size=1)
if re.search('^[0-9]*$', p_major) and \
re.search('^[0-9]*$', p_minor) and (p_size != "1"):
## for some parent devices we have to remove a 'p' (partition)
## an example are partitionable mdadm raid devices (e.g. md1p1)
p_parent = re.sub('p?[1-9]?[0-9]$', '', p_device)
if p_parent == p_device:
if [e for e in ret_list
if re.search('^' + p_parent + 'p?[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 [ get_absolute_devicename(e) for e in ret_list ]
except IOError:
LOGGER.warning("Could not read /proc/partitions")
return []
def get_absolute_devicename(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 = find_major_minor_of_device(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 = find_major_minor_device("/dev/mapper", major, minor)
if result:
return result[0]
## now check all files in /dev
result = find_major_minor_device("/dev", major, minor)
if result:
return result[0]
return default
def find_major_minor_of_device(device):
"""Return the major/minor numbers of a block device.
"""
if re.match("/", device) or \
not os.path.exists(os.path.join(os.path.sep, "sys", "block", device)):
## maybe it is an absolute device name
if not os.path.exists(device):
return None
## okay - it seems to to a device node
rdev = os.stat(device).st_rdev
return (os.major(rdev), os.minor(rdev))
blockdev_info_file = os.path.join(os.path.join(
os.path.sep,"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
return None
def find_major_minor_device(dirpath, major, minor):
"""Returns the names of devices with the specified major and minor number.
"""
collected = []
try:
subdirs = [os.path.join(dirpath, e) for e in os.listdir(dirpath)
if (not os.path.islink(os.path.join(dirpath, e))) and \
os.path.isdir(os.path.join(dirpath, e))]
## do a recursive call to parse the directory tree
for dirs in subdirs:
collected.extend(find_major_minor_device(dirs, major, minor))
## filter all device inodes in this directory
collected.extend([os.path.realpath(os.path.join(dirpath, e))
for e in os.listdir(dirpath)
if (os.major(os.stat(os.path.join(dirpath, e)).st_rdev) == major) \
and (os.minor(os.stat(os.path.join(dirpath, e)).st_rdev) == minor)])
## remove double entries
result = []
for item in collected:
if item not in result:
result.append(item)
return result
except OSError:
return []
def get_parent_blockdevices():
"""Return a list of all block devices that contain other devices.
"""
devs = []
for line in file("/proc/partitions"):
p_details = line.split()
## we expect four values - otherwise continue with next iteration
if len(p_details) != 4:
continue
(p_major, p_minor, p_size, p_device) = p_details
## we expect numeric values in the first two columns
if re.search(r'\D', p_major) or re.search(r'\D', p_minor):
continue
## now let us check, if it is a (parent) block device or a partition
if not os.path.isdir(os.path.join(os.path.sep, "sys", "block", p_device)):
continue
devs.append(p_device)
return [ get_absolute_devicename(e) for e in devs ]
def is_part_of_blockdevice(parent, subdevice):
"""Check if the given block device is a parent of 'subdevice'.
e.g. for checking if a partition belongs to a block device
"""
try:
(par_major, par_minor) = find_major_minor_of_device(parent)
(sub_major, sub_minor) = find_major_minor_of_device(subdevice)
except TypeError:
## at least one of these devices did not return a valid major/minor combination
return False
## search the entry below '/sys/block' belonging to the parent
root = os.path.join(os.path.sep, 'sys', 'block')
for bldev in os.listdir(root):
blpath = os.path.join(root, bldev, 'dev')
if os.access(blpath, os.R_OK):
try:
if (str(par_major), str(par_minor)) == tuple([e
for e in file(blpath)][0].strip().split(":",1)):
parent_path = os.path.join(root, bldev)
break
except (IndexError, OSError):
pass
else:
## no block device with this major/minor combination found below '/sys/block'
return False
for subbldev in os.listdir(parent_path):
subblpath = os.path.join(parent_path, subbldev, "dev")
if os.access(subblpath, os.R_OK):
try:
if (str(sub_major), str(sub_minor)) == tuple([e
for e in file(subblpath)][0].strip().split(":",1)):
## the name of the subdevice node is not important - we found it!
return True
except (IndexError, OSError):
pass
return False
def get_blockdevice_size(device):
"""Return the size of a blockdevice in megabyte.
"""
if not device:
return -1
try:
rdev = os.stat(device).st_rdev
except OSError:
return -1
minor = os.minor(rdev)
major = os.major(rdev)
for line in file("/proc/partitions"):
try:
elements = line.split()
if len(elements) != 4:
continue
if (int(elements[0]) == major) and (int(elements[1]) == minor):
return int(elements[2])/1024
except ValueError:
pass
return -1
def get_blockdevice_size_humanly(device):
"""Return a human readable size of a blockdevice.
"""
size = get_blockdevice_size(device)
if size > 5120:
return "%dGB" % int(size/1024)
else:
return "%dMB" % size

2
src/cryptobox/web/sites.py

@ -324,7 +324,7 @@ class WebInterfaceSites:
## it will get ignored for non-volume plugins
plugin.device = None
if device and self.__set_device(device):
plugin.device = self.cbox.get_container(device).device
plugin.device = self.cbox.get_container(device)
## check the device argument of volume plugins
if "volume" in plugin.plugin_capabilities:
## initialize the dataset of the selected device if necessary

Loading…
Cancel
Save