Commit b31393a1 authored by Iustin Pop's avatar Iustin Pop

Merge branch 'devel-2.4' into stable-2.4

* devel-2.4: (23 commits)
  Fix pylint warnings
  Change the list formatting to a 'special' chars
  Add support for merging node groups
  Add option to rename groups on conflict
  Fix minor docstring typo
  Fix HV/OS parameter validation on non-vm nodes
  NodeQuery: mark live fields as UNAVAIL for non-vm_capable nodes
  NodeQuery: don't query non-vm_capable nodes
  Remove superfluous redundant requirement
  Don't remove master_candidate flag from merged nodes
  Use a consistent ECID base
  listrunner: convert from getopt to optparse
  listrunner: fix agent usage
  Revert "Disable the cluster-merge tool for the moment"
  Fix cluster-merging by not stopping noded
  Fix error msg for instances on offline nodes
  Minor reordering to match param order
  cluster verify and instance disks on offline nodes
  Cluster verify and N+1 warnings for offline nodes
  Handle gnt-instance shutdown --all for empty clusters
  Use gnt-node add --force-join to add foreign nodes
  Add --force-join option to gnt-node add
  Fix iterating over node groups

Of the above commits present in the devel-2.4 branch, only the “Add
--force-join option to gnt-node add” is a potential issue, but this
has been QA-ed successfully. The other fixes are split in three
groups:

- non-core changes (cluster-merge, listrunner)
- trivial fixes (docstrings, etc.)
- bugs that we want fixed

As such, instead of cherry-picking only individual patches, I propose
that we unify stable and devel 2.4 and make a new RC out of the
result.
Signed-off-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarMichael Hanselmann <hansmi@google.com>
parents 0e265161 9b945588
......@@ -23,8 +23,8 @@ clusters into.
The usage of ``cluster-merge`` is as follows::
cluster-merge [--debug|--verbose] [--watcher-pause-period SECONDS] <cluster> \
<cluster...>
cluster-merge [--debug|--verbose] [--watcher-pause-period SECONDS] \
[--groups [merge|rename]] <cluster> [<cluster...>]
You can provide multiple clusters. The tool will then go over every
cluster in serial and perform the steps to merge it into the invoking
......@@ -39,6 +39,15 @@ These options can be used to control the behaviour of the tool:
``--watcher-pause-period``
Define the period of time in seconds the watcher shall be disabled,
default is 1800 seconds (30 minutes).
``--groups``
This option controls how ``cluster-merge`` handles duplicate node
group names on the merging clusters. If ``merge`` is specified then
all node groups with the same name will be merged into one. If
``rename`` is specified, then conflicting node groups on the remove
clusters will have their cluster name appended to the group name. If
this option is not speicifed, then ``cluster-merge`` will refuse to
continue if it finds conflicting group names, otherwise it will
proceed as normal.
Rollback
......
......@@ -294,9 +294,6 @@ failover:
determined by the serial number on the configuration and
highest job ID on the job queue)
- there is not even a single node having a newer
configuration file
- if we are not failing over (but just starting), the
quorum agrees that we are the designated master
......
......@@ -104,6 +104,7 @@ __all__ = [
"NEW_RAPI_CERT_OPT",
"NEW_SECONDARY_OPT",
"NIC_PARAMS_OPT",
"NODE_FORCE_JOIN_OPT",
"NODE_LIST_OPT",
"NODE_PLACEMENT_OPT",
"NODEGROUP_OPT",
......@@ -885,6 +886,11 @@ NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
default=True, action="store_false",
help="Disable SSH key fingerprint checking")
NODE_FORCE_JOIN_OPT = cli_option("--force-join", dest="force_join",
default=False, action="store_true",
help="Force the joining of a node,"
" needed when merging clusters")
MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
type="bool", default=None, metavar=_YORNO,
help="Set the master_candidate flag on the node")
......@@ -2376,17 +2382,23 @@ class _QueryColumnFormatter:
"""Callable class for formatting fields of a query.
"""
def __init__(self, fn, status_fn):
def __init__(self, fn, status_fn, verbose):
"""Initializes this class.
@type fn: callable
@param fn: Formatting function
@type status_fn: callable
@param status_fn: Function to report fields' status
@type verbose: boolean
@param verbose: whether to use verbose field descriptions or not
"""
self._fn = fn
self._status_fn = status_fn
if verbose:
self._desc_index = 0
else:
self._desc_index = 1
def __call__(self, data):
"""Returns a field's string representation.
......@@ -2403,23 +2415,14 @@ class _QueryColumnFormatter:
assert value is None, \
"Found value %r for abnormal status %s" % (value, status)
if status == constants.RS_UNKNOWN:
return "(unknown)"
if status == constants.RS_NODATA:
return "(nodata)"
if status == constants.RS_UNAVAIL:
return "(unavail)"
if status == constants.RS_OFFLINE:
return "(offline)"
if status in constants.RSS_DESCRIPTION:
return constants.RSS_DESCRIPTION[status][self._desc_index]
raise NotImplementedError("Unknown status %s" % status)
def FormatQueryResult(result, unit=None, format_override=None, separator=None,
header=False):
header=False, verbose=False):
"""Formats data in L{objects.QueryResponse}.
@type result: L{objects.QueryResponse}
......@@ -2434,6 +2437,8 @@ def FormatQueryResult(result, unit=None, format_override=None, separator=None,
@param separator: String used to separate fields
@type header: bool
@param header: Whether to output header row
@type verbose: boolean
@param verbose: whether to use verbose field descriptions or not
"""
if unit is None:
......@@ -2456,7 +2461,8 @@ def FormatQueryResult(result, unit=None, format_override=None, separator=None,
assert fdef.title and fdef.name
(fn, align_right) = _GetColumnFormatter(fdef, format_override, unit)
columns.append(TableColumn(fdef.title,
_QueryColumnFormatter(fn, _RecordStatus),
_QueryColumnFormatter(fn, _RecordStatus,
verbose),
align_right))
table = FormatTable(result.data, columns, header, separator)
......@@ -2505,7 +2511,7 @@ def _WarnUnknownFields(fdefs):
def GenericList(resource, fields, names, unit, separator, header, cl=None,
format_override=None):
format_override=None, verbose=False):
"""Generic implementation for listing all items of a resource.
@param resource: One of L{constants.QR_OP_LUXI}
......@@ -2524,6 +2530,8 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
@type format_override: dict
@param format_override: Dictionary for overriding field formatting functions,
indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
@type verbose: boolean
@param verbose: whether to use verbose field descriptions or not
"""
if cl is None:
......@@ -2538,7 +2546,8 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
(status, data) = FormatQueryResult(response, unit=unit, separator=separator,
header=header,
format_override=format_override)
format_override=format_override,
verbose=verbose)
for line in data:
ToStdout(line)
......
......@@ -503,7 +503,7 @@ def ListLocks(opts, args): # pylint: disable-msg=W0613
while True:
ret = GenericList(constants.QR_LOCK, selected_fields, None, None,
opts.separator, not opts.no_headers,
format_override=fmtoverride)
format_override=fmtoverride, verbose=opts.verbose)
if ret != constants.EXIT_SUCCESS:
return ret
......@@ -575,7 +575,8 @@ commands = {
TestJobqueue, ARGS_NONE, [PRIORITY_OPT],
"", "Test a few aspects of the job queue"),
"locks": (
ListLocks, ARGS_NONE, [NOHDR_OPT, SEP_OPT, FIELDS_OPT, INTERVAL_OPT],
ListLocks, ARGS_NONE,
[NOHDR_OPT, SEP_OPT, FIELDS_OPT, INTERVAL_OPT, VERBOSE_OPT],
"[--interval N]", "Show a list of locks in the master daemon"),
}
......
#
#
# Copyright (C) 2010 Google Inc.
# Copyright (C) 2010, 2011 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
......@@ -101,7 +101,7 @@ def ListGroups(opts, args):
return GenericList(constants.QR_GROUP, desired_fields, args, None,
opts.separator, not opts.no_headers,
format_override=fmtoverride)
format_override=fmtoverride, verbose=opts.verbose)
def ListGroupFields(opts, args):
......@@ -121,7 +121,7 @@ def ListGroupFields(opts, args):
def SetGroupParams(opts, args):
"""Modifies a node group's parameters.
@param opts: the command line options seletect by the user
@param opts: the command line options selected by the user
@type args: list
@param args: should contain only one element, the node group name
......@@ -189,7 +189,7 @@ commands = {
"<group_name> <node>...", "Assign nodes to a group"),
"list": (
ListGroups, ARGS_MANY_GROUPS,
[NOHDR_OPT, SEP_OPT, FIELDS_OPT],
[NOHDR_OPT, SEP_OPT, FIELDS_OPT, VERBOSE_OPT],
"[<group_name>...]",
"Lists the node groups in the cluster. The available fields can be shown"
" using the \"list-fields\" command (see the man page for details)."
......
......@@ -217,6 +217,9 @@ def GenericManyOps(operation, fn):
cl = GetClient()
inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
if not inames:
if opts.multi_mode == _SHUTDOWN_CLUSTER:
ToStdout("Cluster is empty, no instances to shutdown")
return 0
raise errors.OpPrereqError("Selection filter does not match"
" any instances", errors.ECODE_INVAL)
multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
......@@ -254,7 +257,7 @@ def ListInstances(opts, args):
return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
opts.separator, not opts.no_headers,
format_override=fmtoverride)
format_override=fmtoverride, verbose=opts.verbose)
def ListInstanceFields(opts, args):
......@@ -1395,7 +1398,7 @@ commands = {
"Show information on the specified instance(s)"),
'list': (
ListInstances, ARGS_MANY_INSTANCES,
[NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
[NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
"[<instance>...]",
"Lists the instances and their status. The available fields can be shown"
" using the \"list-fields\" command (see the man page for details)."
......
......@@ -134,6 +134,8 @@ def _RunSetupSSH(options, nodes):
cmd.append("--verbose")
if not options.ssh_key_check:
cmd.append("--no-ssh-key-check")
if options.force_join:
cmd.append("--force-join")
cmd.extend(nodes)
......@@ -221,7 +223,7 @@ def ListNodes(opts, args):
return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
opts.separator, not opts.no_headers,
format_override=fmtoverride)
format_override=fmtoverride, verbose=opts.verbose)
def ListNodeFields(opts, args):
......@@ -715,10 +717,11 @@ def SetNodeParams(opts, args):
commands = {
'add': (
AddNode, [ArgHost(min=1, max=1)],
[SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
CAPAB_VM_OPT, NODE_PARAMS_OPT],
"[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
[SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
"[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
" [--no-node-setup] [--verbose]"
" <node_name>",
"Add a node to the cluster"),
'evacuate': (
......@@ -744,7 +747,7 @@ commands = {
"[<node_name>...]", "Show information about the node(s)"),
'list': (
ListNodes, ARGS_MANY_NODES,
[NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
[NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
"[nodes...]",
"Lists the nodes in the cluster. The available fields can be shown using"
" the \"list-fields\" command (see the man page for details)."
......
......@@ -1554,7 +1554,7 @@ class LUClusterVerify(LogicalUnit):
node_current)
for node, n_img in node_image.items():
if (not node == node_current):
if node != node_current:
test = instance in n_img.instances
_ErrorIf(test, self.EINSTANCEWRONGNODE, instance,
"instance should not run on node %s", node)
......@@ -1564,7 +1564,11 @@ class LUClusterVerify(LogicalUnit):
for idx, (success, status) in enumerate(disks)]
for nname, success, bdev_status, idx in diskdata:
_ErrorIf(instanceconfig.admin_up and not success,
# the 'ghost node' construction in Exec() ensures that we have a
# node here
snode = node_image[nname]
bad_snode = snode.ghost or snode.offline
_ErrorIf(instanceconfig.admin_up and not success and not bad_snode,
self.EINSTANCEFAULTYDISK, instance,
"couldn't retrieve status for disk/%s on %s: %s",
idx, nname, bdev_status)
......@@ -1622,6 +1626,12 @@ class LUClusterVerify(LogicalUnit):
# WARNING: we currently take into account down instances as well
# as up ones, considering that even if they're down someone
# might want to start them even in the event of a node failure.
if n_img.offline:
# we're skipping offline nodes from the N+1 warning, since
# most likely we don't have good memory infromation from them;
# we already list instances living on such nodes, and that's
# enough warning
continue
for prinode, instances in n_img.sbp.items():
needed_mem = 0
for instance in instances:
......@@ -2290,8 +2300,8 @@ class LUClusterVerify(LogicalUnit):
self.ENODERPC, pnode, "instance %s, connection to"
" primary node failed", instance)
if pnode_img.offline:
inst_nodes_offline.append(pnode)
_ErrorIf(pnode_img.offline, self.EINSTANCEBADNODE, instance,
"instance lives on offline node %s", inst_config.primary_node)
# If the instance is non-redundant we cannot survive losing its primary
# node, so we are not N+1 compliant. On the other hand we have no disk
......@@ -2340,7 +2350,7 @@ class LUClusterVerify(LogicalUnit):
# warn that the instance lives on offline nodes
_ErrorIf(inst_nodes_offline, self.EINSTANCEBADNODE, instance,
"instance lives on offline node(s) %s",
"instance has offline secondary node(s) %s",
utils.CommaJoin(inst_nodes_offline))
# ... or ghost/non-vm_capable nodes
for node in inst_config.all_nodes:
......@@ -3622,7 +3632,10 @@ class _NodeQuery(_QueryBase):
# Gather data as requested
if query.NQ_LIVE in self.requested_data:
node_data = lu.rpc.call_node_info(nodenames, lu.cfg.GetVGName(),
# filter out non-vm_capable nodes
toquery_nodes = [name for name in nodenames if all_info[name].vm_capable]
node_data = lu.rpc.call_node_info(toquery_nodes, lu.cfg.GetVGName(),
lu.cfg.GetHypervisorType())
live_data = dict((name, nresult.payload)
for (name, nresult) in node_data.items()
......@@ -6808,6 +6821,21 @@ def _ComputeDiskSize(disk_template, disks):
return req_size_dict[disk_template]
def _FilterVmNodes(lu, nodenames):
"""Filters out non-vm_capable nodes from a list.
@type lu: L{LogicalUnit}
@param lu: the logical unit for which we check
@type nodenames: list
@param nodenames: the list of nodes on which we should check
@rtype: list
@return: the list of vm-capable nodes
"""
vm_nodes = frozenset(lu.cfg.GetNonVmCapableNodeList())
return [name for name in nodenames if name not in vm_nodes]
def _CheckHVParams(lu, nodenames, hvname, hvparams):
"""Hypervisor parameter validation.
......@@ -6825,6 +6853,7 @@ def _CheckHVParams(lu, nodenames, hvname, hvparams):
@raise errors.OpPrereqError: if the parameters are not valid
"""
nodenames = _FilterVmNodes(lu, nodenames)
hvinfo = lu.rpc.call_hypervisor_validate_params(nodenames,
hvname,
hvparams)
......@@ -6852,6 +6881,7 @@ def _CheckOSParams(lu, required, nodenames, osname, osparams):
@raise errors.OpPrereqError: if the parameters are not valid
"""
nodenames = _FilterVmNodes(lu, nodenames)
result = lu.rpc.call_os_validate(required, nodenames, osname,
[constants.OS_VALIDATE_PARAMETERS],
osparams)
......
......@@ -1029,6 +1029,14 @@ RS_ALL = frozenset([
RS_OFFLINE,
])
#: Dictionary with special field cases and their verbose/terse formatting
RSS_DESCRIPTION = {
RS_UNKNOWN: ("(unknown)", "??"),
RS_NODATA: ("(nodata)", "?"),
RS_OFFLINE: ("(offline)", "*"),
RS_UNAVAIL: ("(unavail)", "-"),
}
# max dynamic devices
MAX_NICS = 8
MAX_DISKS = 16
......
......@@ -487,6 +487,7 @@ _NODE_SIMPLE_FIELDS = {
#: Fields requiring talking to the node
# Note that none of these are available for non-vm_capable nodes
_NODE_LIVE_FIELDS = {
"bootid": ("BootID", QFT_TEXT, "bootid"),
"cnodes": ("CNodes", QFT_NUMBER, "cpu_nodes"),
......@@ -577,6 +578,9 @@ def _GetLiveNodeField(field, kind, ctx, node):
if node.offline:
return _FS_OFFLINE
if not node.vm_capable:
return _FS_UNAVAIL
if not ctx.curlive_data:
return _FS_NODATA
......
......@@ -179,3 +179,38 @@ Common daemon functionality
All Ganeti daemons re-open the log file(s) when sent a SIGHUP signal.
**logrotate**(8) can be used to rotate Ganeti's log files.
Common field formatting
-----------------------
Multiple ganeti commands use the same framework for tabular listing of
resources (e.g. **gnt-instance list**, **gnt-node list**, **gnt-group
list**, **gnt-debug locks**, etc.). For these commands, special states
are denoted via a special symbol (in terse mode) or a string (in
verbose mode):
*, (offline)
The node in question is marked offline, and thus it cannot be
queried for data. This result is persistent until the node is
de-offlined.
?, (nodata)
Ganeti expected to receive an answer from this entity, but the
cluster RPC call failed and/or we didn't receive a valid answer;
usually more information is available in the node daemon log (if
the node is alive) or the master daemon log. This result is
transient, and re-running command might return a different result.
-, (unavail)
The respective field doesn't make sense for this entity;
e.g. querying a down instance for its current memory 'live' usage,
or querying a non-vm_capable node for disk/memory data. This
result is persistent, and until the entity state is changed via
ganeti commands, the result won't change.
??, (unknown)
This field is not known (note that this is different from entity
being unknown). Either you have mis-typed the field name, or you
are using a field that the running Ganeti master daemon doesn't
know. This result is persistent, re-running the command won't
change it.
......@@ -93,7 +93,7 @@ failed jobs deliberately.
LOCKS
~~~~~
| **locks** [--no-headers] [--separator=*SEPARATOR*]
| **locks** [--no-headers] [--separator=*SEPARATOR*] [-v]
| [-o *[+]FIELD,...*] [--interval=*SECONDS*]
Shows a list of locks in the master daemon.
......@@ -103,6 +103,9 @@ The ``--no-headers`` option will skip the initial header line. The
used between the output fields. Both these options are to help
scripting.
The ``-v`` option activates verbose mode, which changes the display of
special field states (see **ganeti(7)**).
The ``-o`` option takes a comma-separated list of output fields.
The available fields and their meaning are:
......
......@@ -92,7 +92,7 @@ least one group, so the last group cannot be removed.
LIST
~~~~
| **list** [--no-headers] [--separator=*SEPARATOR*]
| **list** [--no-headers] [--separator=*SEPARATOR*] [-v]
| [-o *[+]FIELD,...*] [group...]
Lists all existing node groups in the cluster.
......@@ -102,6 +102,9 @@ The ``--no-headers`` option will skip the initial header line. The
used between the output fields. Both these options are to help
scripting.
The ``-v`` option activates verbose mode, which changes the display of
special field states (see **ganeti(7)**).
The ``-o`` option takes a comma-separated list of output fields.
If the value of the option starts with the character ``+``, the new
fields will be added to the default list. This allows to quickly
......
......@@ -624,7 +624,7 @@ LIST
^^^^
| **list**
| [--no-headers] [--separator=*SEPARATOR*] [--units=*UNITS*]
| [--no-headers] [--separator=*SEPARATOR*] [--units=*UNITS*] [-v]
| [-o *[+]FIELD,...*] [instance...]
Shows the currently configured instances with memory usage, disk
......@@ -642,11 +642,13 @@ option is given, then the values are shown in mebibytes to allow
parsing by scripts. In both cases, the ``--units`` option can be
used to enforce a given output unit.
The ``-v`` option activates verbose mode, which changes the display of
special field states (see **ganeti(7)**).
The ``-o`` option takes a comma-separated list of output fields.
The available fields and their meaning are:
name
the instance name
......
......@@ -49,6 +49,10 @@ secondary IP again, it will reused from the cluster. Also, the
drained and offline flags of the node will be cleared before
re-adding it.
The ``--force-join`` option is to proceed with adding a node even if it already
appears to belong to another cluster. This is used during cluster merging, for
example.
The ``-g`` is used to add the new node into a specific node group,
specified by UUID or name. If only one node group exists you can
skip this option, otherwise it's mandatory.
......@@ -145,7 +149,7 @@ LIST
| **list**
| [--no-headers] [--separator=*SEPARATOR*]
| [--units=*UNITS*] [-o *[+]FIELD,...*]
| [--units=*UNITS*] [-v] [-o *[+]FIELD,...*]
| [node...]
Lists the nodes in the cluster.
......@@ -165,11 +169,13 @@ used to enforce a given output unit.
Queries of nodes will be done in parallel with any running jobs. This might
give inconsistent results for the free disk/memory.
The ``-v`` option activates verbose mode, which changes the display of
special field states (see **ganeti(7)**).
The ``-o`` option takes a comma-separated list of output fields.
The available fields and their meaning are:
name
the node name
......
#!/usr/bin/python
#
# Copyright (C) 2008 Google Inc.
# Copyright (C) 2008, 2011 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
......@@ -396,13 +396,21 @@ class TestFormatQueryResult(unittest.TestCase):
])
self.assertEqual(cli.FormatQueryResult(response, header=True,
separator="|"),
separator="|", verbose=True),
(cli.QR_UNKNOWN, [
"ID|unk|Unavail|NoData|OffLine",
"1|(unknown)|N||(offline)",
"2|(unknown)|(nodata)|x|(offline)",
"3|(unknown)|N|(unavail)|(offline)",
]))
self.assertEqual(cli.FormatQueryResult(response, header=True,
separator="|", verbose=False),
(cli.QR_UNKNOWN, [
"ID|unk|Unavail|NoData|OffLine",
"1|??|N||*",
"2|??|?|x|*",
"3|??|N|-|*",
]))
def testNoData(self):
fields = [
......@@ -452,12 +460,19 @@ class TestFormatQueryResult(unittest.TestCase):
])
self.assertEqual(cli.FormatQueryResult(response, header=False,
separator="|"),
separator="|", verbose=True),
(cli.QR_INCOMPLETE, [
"1|N||(offline)",
"2|(nodata)|x|abc",
"3|N|(unavail)|(offline)",
]))
self.assertEqual(cli.FormatQueryResult(response, header=False,
separator="|", verbose=False),
(cli.QR_INCOMPLETE, [
"1|N||*",
"2|?|x|abc",
"3|N|-|*",
]))
def testInvalidFieldType(self):
fields = [
......
......@@ -355,7 +355,7 @@ class TestNodeQuery(unittest.TestCase):
master_candidate=(name != master_name and idx % 3 == 0),
offline=False,
drained=False,
vm_capable=False,
vm_capable=True,
master_capable=False,
ndparams={},
group="default",
......@@ -468,10 +468,16 @@ class TestNodeQuery(unittest.TestCase):
def testGetLiveNodeField(self):
nodes = [
objects.Node(name="node1", drained=False, offline=False),
objects.Node(name="node2", drained=True, offline=False),
objects.Node(name="node3", drained=False, offline=False),
objects.Node(name="node4", drained=False, offline=True),
objects.Node(name="node1", drained=False, offline=False,
vm_capable=True),
objects.Node(name="node2", drained=True, offline=False,
vm_capable=True),
objects.Node(name="node3", drained=False, offline=False,
vm_capable=True),
objects.Node(name="node4", drained=False, offline=True,
vm_capable=True),
objects.Node(name="node5", drained=False, offline=False,
vm_capable=False),