Commit fe502d25 authored by Iustin Pop's avatar Iustin Pop

Merge branch 'devel-2.7'

* devel-2.7:
  Rename lib/objectutils to outils.py
  Fix typo in gnt-group manpage
  Fix wrong type in a docstring of the RAPI subsystem
  Finish the remote→restricted commands rename
  Enable use of the priority option in hbal
  Add CLI-level option to override the priority
  Add functions to parse CLI-level format of priorities
  Add a function to change an OpCode's priority
  Make hbal opcode annotation more generic
  Add unit tests for RADOSBLockDevice
  Fix rbd showmapped output parsing
  Change default xen root path to /dev/xvda1
  Removes check for conflicts from NetworkDisconnect
  If _UnlockedLookupNetwork() fails raise error
  Force conflicts check in LUNetworkDisconnect

Also updated objects.py with more outils renames.
Signed-off-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarGuido Trotter <ultrotter@google.com>
parents d9a22528 473d87a3
......@@ -106,6 +106,7 @@ DIRS = \
qa \
test \
test/data \
test/data/bdev-rbd \
test/data/ovfdata \
test/data/ovfdata/other \
test/py \
......@@ -260,8 +261,8 @@ pkgpython_PYTHON = \
lib/mcpu.py \
lib/netutils.py \
lib/objects.py \
lib/objectutils.py \
lib/opcodes.py \
lib/outils.py \
lib/ovf.py \
lib/pathutils.py \
lib/qlang.py \
......@@ -394,8 +395,8 @@ docinput = \
doc/design-partitioned.rst \
doc/design-query-splitting.rst \
doc/design-query2.rst \
doc/design-remote-commands.rst \
doc/design-resource-model.rst \
doc/design-restricted-commands.rst \
doc/design-shared-storage.rst \
doc/design-monitoring-agent.rst \
doc/design-virtual-clusters.rst \
......@@ -989,6 +990,18 @@ TEST_FILES = \
test/data/bdev-drbd-disk.txt \
test/data/bdev-drbd-net-ip4.txt \
test/data/bdev-drbd-net-ip6.txt \
test/data/bdev-rbd/json_output_empty.txt \
test/data/bdev-rbd/json_output_extra_matches.txt \
test/data/bdev-rbd/json_output_no_matches.txt \
test/data/bdev-rbd/json_output_ok.txt \
test/data/bdev-rbd/plain_output_new_extra_matches.txt \
test/data/bdev-rbd/plain_output_new_no_matches.txt \
test/data/bdev-rbd/plain_output_new_ok.txt \
test/data/bdev-rbd/plain_output_old_empty.txt \
test/data/bdev-rbd/plain_output_old_extra_matches.txt \
test/data/bdev-rbd/plain_output_old_no_matches.txt \
test/data/bdev-rbd/plain_output_old_ok.txt \
test/data/bdev-rbd/output_invalid.txt \
test/data/cert1.pem \
test/data/cert2.pem \
test/data/instance-minor-pairing.txt \
......@@ -1088,8 +1101,8 @@ python_tests = \
test/py/ganeti.mcpu_unittest.py \
test/py/ganeti.netutils_unittest.py \
test/py/ganeti.objects_unittest.py \
test/py/ganeti.objectutils_unittest.py \
test/py/ganeti.opcodes_unittest.py \
test/py/ganeti.outils_unittest.py \
test/py/ganeti.ovf_unittest.py \
test/py/ganeti.qlang_unittest.py \
test/py/ganeti.query_unittest.py \
......
......@@ -6,7 +6,7 @@ The following design documents have been implemented in Ganeti 2.7:
- :doc:`design-bulk-create`
- :doc:`design-opportunistic-locking`
- :doc:`design-remote-commands`
- :doc:`design-restricted-commands`
- :doc:`design-node-add`
- :doc:`design-virtual-clusters`
- :doc:`design-network`
......
......@@ -28,7 +28,7 @@ be taken:
- No parameters may be passed
- No absolute or relative path may be passed, only a filename
- Executable must reside in ``/etc/ganeti/remote-commands``, which must
- Executable must reside in ``/etc/ganeti/restricted-commands``, which must
be owned by root:root and have mode 0755 or stricter
- Must be regular files or symlinks
- Must be executable by root:root
......@@ -46,7 +46,7 @@ If a command can not be executed for some reason, the lock is only
released with a delay of several seconds, after which the generic error
message will be returned to the caller.
At first, remote commands will not be made available through the
At first, restricted commands will not be made available through the
:doc:`remote API <rapi>`, though that could be done at a later point
(with a separate password).
......
......@@ -51,7 +51,7 @@ Contents:
design-opportunistic-locking.rst
design-ovf-support.rst
design-query2.rst
design-remote-commands.rst
design-restricted-commands.rst
design-shared-storage.rst
design-virtual-clusters.rst
design-network.rst
......
#
#
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -88,15 +88,15 @@ _LVSLINE_REGEX = re.compile("^ *([^|]+)\|([^|]+)\|([0-9.]+)\|([^|]{6,})\|?$")
_MASTER_START = "start"
_MASTER_STOP = "stop"
#: Maximum file permissions for remote command directory and executables
#: Maximum file permissions for restricted command directory and executables
_RCMD_MAX_MODE = (stat.S_IRWXU |
stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
#: Delay before returning an error for remote commands
#: Delay before returning an error for restricted commands
_RCMD_INVALID_DELAY = 10
#: How long to wait to acquire lock for remote commands (shorter than
#: How long to wait to acquire lock for restricted commands (shorter than
#: L{_RCMD_INVALID_DELAY}) to reduce blockage of noded forks when many
#: command requests arrive
_RCMD_LOCK_TIMEOUT = _RCMD_INVALID_DELAY * 0.8
......@@ -3672,7 +3672,7 @@ def PowercycleNode(hypervisor_type):
def _VerifyRestrictedCmdName(cmd):
"""Verifies a remote command name.
"""Verifies a restricted command name.
@type cmd: string
@param cmd: Command name
......@@ -3694,7 +3694,7 @@ def _VerifyRestrictedCmdName(cmd):
def _CommonRestrictedCmdCheck(path, owner):
"""Common checks for remote command file system directories and files.
"""Common checks for restricted command file system directories and files.
@type path: string
@param path: Path to check
......@@ -3724,7 +3724,7 @@ def _CommonRestrictedCmdCheck(path, owner):
def _VerifyRestrictedCmdDirectory(path, _owner=None):
"""Verifies remote command directory.
"""Verifies restricted command directory.
@type path: string
@param path: Path to check
......@@ -3745,10 +3745,10 @@ def _VerifyRestrictedCmdDirectory(path, _owner=None):
def _VerifyRestrictedCmd(path, cmd, _owner=None):
"""Verifies a whole remote command and returns its executable filename.
"""Verifies a whole restricted command and returns its executable filename.
@type path: string
@param path: Directory containing remote commands
@param path: Directory containing restricted commands
@type cmd: string
@param cmd: Command name
@rtype: tuple; (boolean, string)
......@@ -3774,10 +3774,10 @@ def _PrepareRestrictedCmd(path, cmd,
_verify_dir=_VerifyRestrictedCmdDirectory,
_verify_name=_VerifyRestrictedCmdName,
_verify_cmd=_VerifyRestrictedCmd):
"""Performs a number of tests on a remote command.
"""Performs a number of tests on a restricted command.
@type path: string
@param path: Directory containing remote commands
@param path: Directory containing restricted commands
@type cmd: string
@param cmd: Command name
@return: Same as L{_VerifyRestrictedCmd}
......@@ -3804,7 +3804,7 @@ def RunRestrictedCmd(cmd,
_prepare_fn=_PrepareRestrictedCmd,
_runcmd_fn=utils.RunCmd,
_enabled=constants.ENABLE_RESTRICTED_COMMANDS):
"""Executes a remote command after performing strict tests.
"""Executes a restricted command after performing strict tests.
@type cmd: string
@param cmd: Command name
......@@ -3813,10 +3813,10 @@ def RunRestrictedCmd(cmd,
@raise RPCFail: In case of an error
"""
logging.info("Preparing to run remote command '%s'", cmd)
logging.info("Preparing to run restricted command '%s'", cmd)
if not _enabled:
_Fail("Remote commands disabled at configure time")
_Fail("Restricted commands disabled at configure time")
lock = None
try:
......@@ -3844,7 +3844,7 @@ def RunRestrictedCmd(cmd,
# Do not include original error message in returned error
_Fail("Executing command '%s' failed" % cmd)
elif cmdresult.failed or cmdresult.fail_reason:
_Fail("Remote command '%s' failed: %s; output: %s",
_Fail("Restricted command '%s' failed: %s; output: %s",
cmd, cmdresult.fail_reason, cmdresult.output)
else:
return cmdresult.output
......
......@@ -38,12 +38,20 @@ from ganeti import objects
from ganeti import compat
from ganeti import netutils
from ganeti import pathutils
from ganeti import serializer
# Size of reads in _CanReadDevice
_DEVICE_READ_SIZE = 128 * 1024
class RbdShowmappedJsonError(Exception):
"""`rbd showmmapped' JSON formatting error Exception class.
"""
pass
def _IgnoreError(fn, *args, **kwargs):
"""Executes the given function, ignoring BlockDeviceErrors.
......@@ -2726,14 +2734,7 @@ class RADOSBlockDevice(BlockDev):
name = unique_id[1]
# Check if the mapping already exists.
showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
result = utils.RunCmd(showmap_cmd)
if result.failed:
_ThrowError("rbd showmapped failed (%s): %s",
result.fail_reason, result.output)
rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
rbd_dev = self._VolumeToBlockdev(pool, name)
if rbd_dev:
# The mapping exists. Return it.
return rbd_dev
......@@ -2746,14 +2747,7 @@ class RADOSBlockDevice(BlockDev):
result.fail_reason, result.output)
# Find the corresponding rbd device.
showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
result = utils.RunCmd(showmap_cmd)
if result.failed:
_ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
result.fail_reason, result.output)
rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
rbd_dev = self._VolumeToBlockdev(pool, name)
if not rbd_dev:
_ThrowError("rbd map succeeded, but could not find the rbd block"
" device in output of showmapped, for volume: %s", name)
......@@ -2761,16 +2755,93 @@ class RADOSBlockDevice(BlockDev):
# The device was successfully mapped. Return it.
return rbd_dev
@classmethod
def _VolumeToBlockdev(cls, pool, volume_name):
"""Do the 'volume name'-to-'rbd block device' resolving.
@type pool: string
@param pool: RADOS pool to use
@type volume_name: string
@param volume_name: the name of the volume whose device we search for
@rtype: string or None
@return: block device path if the volume is mapped, else None
"""
try:
# Newer versions of the rbd tool support json output formatting. Use it
# if available.
showmap_cmd = [
constants.RBD_CMD,
"showmapped",
"-p",
pool,
"--format",
"json"
]
result = utils.RunCmd(showmap_cmd)
if result.failed:
logging.error("rbd JSON output formatting returned error (%s): %s,"
"falling back to plain output parsing",
result.fail_reason, result.output)
raise RbdShowmappedJsonError
return cls._ParseRbdShowmappedJson(result.output, volume_name)
except RbdShowmappedJsonError:
# For older versions of rbd, we have to parse the plain / text output
# manually.
showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
result = utils.RunCmd(showmap_cmd)
if result.failed:
_ThrowError("rbd showmapped failed (%s): %s",
result.fail_reason, result.output)
return cls._ParseRbdShowmappedPlain(result.output, volume_name)
@staticmethod
def _ParseRbdShowmappedJson(output, volume_name):
"""Parse the json output of `rbd showmapped'.
This method parses the json output of `rbd showmapped' and returns the rbd
block device path (e.g. /dev/rbd0) that matches the given rbd volume.
@type output: string
@param output: the json output of `rbd showmapped'
@type volume_name: string
@param volume_name: the name of the volume whose device we search for
@rtype: string or None
@return: block device path if the volume is mapped, else None
"""
try:
devices = serializer.LoadJson(output)
except ValueError, err:
_ThrowError("Unable to parse JSON data: %s" % err)
rbd_dev = None
for d in devices.values(): # pylint: disable=E1103
try:
name = d["name"]
except KeyError:
_ThrowError("'name' key missing from json object %s", devices)
if name == volume_name:
if rbd_dev is not None:
_ThrowError("rbd volume %s is mapped more than once", volume_name)
rbd_dev = d["device"]
return rbd_dev
@staticmethod
def _ParseRbdShowmappedOutput(output, volume_name):
"""Parse the output of `rbd showmapped'.
def _ParseRbdShowmappedPlain(output, volume_name):
"""Parse the (plain / text) output of `rbd showmapped'.
This method parses the output of `rbd showmapped' and returns
the rbd block device path (e.g. /dev/rbd0) that matches the
given rbd volume.
@type output: string
@param output: the whole output of `rbd showmapped'
@param output: the plain text output of `rbd showmapped'
@type volume_name: string
@param volume_name: the name of the volume whose device we search for
@rtype: string or None
......@@ -2781,30 +2852,31 @@ class RADOSBlockDevice(BlockDev):
volumefield = 2
devicefield = 4
field_sep = "\t"
lines = output.splitlines()
splitted_lines = map(lambda l: l.split(field_sep), lines)
# Check empty output.
# Try parsing the new output format (ceph >= 0.55).
splitted_lines = map(lambda l: l.split(), lines)
# Check for empty output.
if not splitted_lines:
_ThrowError("rbd showmapped returned empty output")
return None
# Check showmapped header line, to determine number of fields.
# Check showmapped output, to determine number of fields.
field_cnt = len(splitted_lines[0])
if field_cnt != allfields:
_ThrowError("Cannot parse rbd showmapped output because its format"
" seems to have changed; expected %s fields, found %s",
allfields, field_cnt)
# Parsing the new format failed. Fallback to parsing the old output
# format (< 0.55).
splitted_lines = map(lambda l: l.split("\t"), lines)
if field_cnt != allfields:
_ThrowError("Cannot parse rbd showmapped output expected %s fields,"
" found %s", allfields, field_cnt)
matched_lines = \
filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
splitted_lines)
if len(matched_lines) > 1:
_ThrowError("The rbd volume %s is mapped more than once."
" This shouldn't happen, try to unmap the extra"
" devices manually.", volume_name)
_ThrowError("rbd volume %s mapped more than once", volume_name)
if matched_lines:
# rbd block device found. Return it.
......@@ -2845,13 +2917,7 @@ class RADOSBlockDevice(BlockDev):
name = unique_id[1]
# Check if the mapping already exists.
showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
result = utils.RunCmd(showmap_cmd)
if result.failed:
_ThrowError("rbd showmapped failed [during unmap](%s): %s",
result.fail_reason, result.output)
rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
rbd_dev = self._VolumeToBlockdev(pool, name)
if rbd_dev:
# The mapping exists. Unmap the rbd device.
......
......@@ -1897,7 +1897,7 @@ HVC_DEFAULTS = {
HV_BOOTLOADER_ARGS: "",
HV_KERNEL_PATH: XEN_KERNEL,
HV_INITRD_PATH: "",
HV_ROOT_PATH: "/dev/sda1",
HV_ROOT_PATH: "/dev/xvda1",
HV_KERNEL_ARGS: "ro",
HV_MIGRATION_PORT: 8002,
HV_MIGRATION_MODE: HT_MIGRATION_LIVE,
......
#
#
# Copyright (C) 2012 Google Inc.
# Copyright (C) 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -25,7 +25,7 @@ from ganeti import compat
from ganeti import constants
from ganeti import errors
from ganeti import ht
from ganeti import objectutils
from ganeti import outils
from ganeti import opcodes
from ganeti import rpc
from ganeti import serializer
......@@ -60,7 +60,7 @@ _NEVAC_RESULT = ht.TAnd(ht.TIsLength(3),
_INST_NAME = ("name", ht.TNonEmptyString)
class _AutoReqParam(objectutils.AutoSlots):
class _AutoReqParam(outils.AutoSlots):
"""Meta class for request definitions.
"""
......@@ -73,7 +73,7 @@ class _AutoReqParam(objectutils.AutoSlots):
return [slot for (slot, _) in params]
class IARequestBase(objectutils.ValidatedSlots):
class IARequestBase(outils.ValidatedSlots):
"""A generic IAllocator request object.
"""
......@@ -92,7 +92,7 @@ class IARequestBase(objectutils.ValidatedSlots):
REQ_PARAMS attribute for this class.
"""
objectutils.ValidatedSlots.__init__(self, **kwargs)
outils.ValidatedSlots.__init__(self, **kwargs)
self.Validate()
......
#
#
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -45,7 +45,7 @@ from cStringIO import StringIO
from ganeti import errors
from ganeti import constants
from ganeti import netutils
from ganeti import objectutils
from ganeti import outils
from ganeti import utils
from socket import AF_INET
......@@ -193,7 +193,7 @@ def MakeEmptyIPolicy():
])
class ConfigObject(objectutils.ValidatedSlots):
class ConfigObject(outils.ValidatedSlots):
"""A generic config object.
It has the following properties:
......@@ -406,7 +406,7 @@ class ConfigData(ConfigObject):
mydict = super(ConfigData, self).ToDict()
mydict["cluster"] = mydict["cluster"].ToDict()
for key in "nodes", "instances", "nodegroups", "networks":
mydict[key] = objectutils.ContainerToDicts(mydict[key])
mydict[key] = outils.ContainerToDicts(mydict[key])
return mydict
......@@ -417,12 +417,12 @@ class ConfigData(ConfigObject):
"""
obj = super(ConfigData, cls).FromDict(val)
obj.cluster = Cluster.FromDict(obj.cluster)
obj.nodes = objectutils.ContainerFromDicts(obj.nodes, dict, Node)
obj.nodes = outils.ContainerFromDicts(obj.nodes, dict, Node)
obj.instances = \
objectutils.ContainerFromDicts(obj.instances, dict, Instance)
outils.ContainerFromDicts(obj.instances, dict, Instance)
obj.nodegroups = \
objectutils.ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
obj.networks = objectutils.ContainerFromDicts(obj.networks, dict, Network)
outils.ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
obj.networks = outils.ContainerFromDicts(obj.networks, dict, Network)
return obj
def HasAnyDiskOfType(self, dev_type):
......@@ -732,7 +732,7 @@ class Disk(ConfigObject):
for attr in ("children",):
alist = bo.get(attr, None)
if alist:
bo[attr] = objectutils.ContainerToDicts(alist)
bo[attr] = outils.ContainerToDicts(alist)
return bo
@classmethod
......@@ -742,7 +742,7 @@ class Disk(ConfigObject):
"""
obj = super(Disk, cls).FromDict(val)
if obj.children:
obj.children = objectutils.ContainerFromDicts(obj.children, list, Disk)
obj.children = outils.ContainerFromDicts(obj.children, list, Disk)
if obj.logical_id and isinstance(obj.logical_id, list):
obj.logical_id = tuple(obj.logical_id)
if obj.physical_id and isinstance(obj.physical_id, list):
......@@ -1100,7 +1100,7 @@ class Instance(TaggableObject):
for attr in "nics", "disks":
alist = bo.get(attr, None)
if alist:
nlist = objectutils.ContainerToDicts(alist)
nlist = outils.ContainerToDicts(alist)
else:
nlist = []
bo[attr] = nlist
......@@ -1119,8 +1119,8 @@ class Instance(TaggableObject):
if "admin_up" in val:
del val["admin_up"]
obj = super(Instance, cls).FromDict(val)
obj.nics = objectutils.ContainerFromDicts(obj.nics, list, NIC)
obj.disks = objectutils.ContainerFromDicts(obj.disks, list, Disk)
obj.nics = outils.ContainerFromDicts(obj.nics, list, NIC)
obj.disks = outils.ContainerFromDicts(obj.disks, list, Disk)
return obj
def UpgradeConfig(self):
......@@ -1314,12 +1314,12 @@ class Node(TaggableObject):
hv_state = data.get("hv_state", None)
if hv_state is not None:
data["hv_state"] = objectutils.ContainerToDicts(hv_state)
data["hv_state"] = outils.ContainerToDicts(hv_state)
disk_state = data.get("disk_state", None)
if disk_state is not None:
data["disk_state"] = \
dict((key, objectutils.ContainerToDicts(value))
dict((key, outils.ContainerToDicts(value))
for (key, value) in disk_state.items())
return data
......@@ -1333,11 +1333,11 @@ class Node(TaggableObject):
if obj.hv_state is not None:
obj.hv_state = \
objectutils.ContainerFromDicts(obj.hv_state, dict, NodeHvState)
outils.ContainerFromDicts(obj.hv_state, dict, NodeHvState)
if obj.disk_state is not None:
obj.disk_state = \
dict((key, objectutils.ContainerFromDicts(value, dict, NodeDiskState))
dict((key, outils.ContainerFromDicts(value, dict, NodeDiskState))
for (key, value) in obj.disk_state.items())
return obj
......@@ -1906,7 +1906,7 @@ class _QueryResponseBase(ConfigObject):
"""
mydict = super(_QueryResponseBase, self).ToDict()
mydict["fields"] = objectutils.ContainerToDicts(mydict["fields"])
mydict["fields"] = outils.ContainerToDicts(mydict["fields"])
return mydict
@classmethod
......@@ -1916,7 +1916,7 @@ class _QueryResponseBase(ConfigObject):
"""
obj = super(_QueryResponseBase, cls).FromDict(val)
obj.fields = \
objectutils.ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
outils.ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
return obj
......
#
#
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -41,7 +41,7 @@ from ganeti import constants
from ganeti import errors
from ganeti import ht
from ganeti import objects
from ganeti import objectutils
from ganeti import outils
# Common opcode attributes
......@@ -417,7 +417,7 @@ _TIpNetwork6 = ht.TAnd(ht.TString, _CheckCIDR6NetNotation)
_TMaybeAddr4List = ht.TMaybe(ht.TListOf(_TIpAddress4))
class _AutoOpParamSlots(objectutils.AutoSlots):
class _AutoOpParamSlots(outils.AutoSlots):
"""Meta class for opcode definitions.
"""
......@@ -443,7 +443,7 @@ class _AutoOpParamSlots(objectutils.AutoSlots):
attrs["OP_ID"] = _NameToId(name)
return objectutils.AutoSlots.__new__(mcs, name, bases, attrs)
return outils.AutoSlots.__new__(mcs, name, bases, attrs)
@classmethod
def _GetSlots(mcs, attrs):
......@@ -457,7 +457,7 @@ class _AutoOpParamSlots(objectutils.AutoSlots):
return [pname for (pname, _, _, _) in params]
class BaseOpCode(objectutils.ValidatedSlots):
class BaseOpCode(outils.ValidatedSlots):
"""A simple serializable object.
This object serves as a parent class for OpCode without any custom
......
......@@ -298,7 +298,7 @@ class ResourceBase(object):
return val
def _checkStringVariable(self, name, default=None):
"""Return the parsed value of an int argument.
"""Return the parsed value of a string argument.
"""
val = self.queryargs.get(name, default)
......
......@@ -32,7 +32,7 @@ ADD
| [\--specs-disk-size *spec-param*=*value* [,*spec-param*=*value*...]]