blob: 67ea37551553200f5422a25ccb936d1c2d044105 [file] [log] [blame]
#
#
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Module dealing with command line parsing"""
import sys
import textwrap
import os.path
import time
import logging
import errno
import itertools
import shlex
from cStringIO import StringIO
from ganeti import utils
from ganeti import errors
from ganeti import constants
from ganeti import opcodes
import ganeti.rpc.errors as rpcerr
from ganeti import ssh
from ganeti import compat
from ganeti import netutils
from ganeti import qlang
from ganeti import objects
from ganeti import pathutils
from ganeti import serializer
import ganeti.cli_opts
# Import constants
from ganeti.cli_opts import * # pylint: disable=W0401
from ganeti.runtime import (GetClient)
from optparse import (OptionParser, TitledHelpFormatter)
__all__ = [
# Generic functions for CLI programs
"ConfirmOperation",
"CreateIPolicyFromOpts",
"GenericMain",
"GenericInstanceCreate",
"GenericList",
"GenericListFields",
"GetClient",
"GetOnlineNodes",
"GetNodesSshPorts",
"GetNodeUUIDs",
"JobExecutor",
"ParseTimespec",
"RunWhileClusterStopped",
"RunWhileDaemonsStopped",
"SubmitOpCode",
"SubmitOpCodeToDrainedQueue",
"SubmitOrSend",
# Formatting functions
"ToStderr", "ToStdout",
"ToStdoutAndLoginfo",
"FormatError",
"FormatQueryResult",
"FormatParamsDictInfo",
"FormatPolicyInfo",
"PrintIPolicyCommand",
"PrintGenericInfo",
"GenerateTable",
"AskUser",
"FormatTimestamp",
"FormatLogMessage",
# Tags functions
"ListTags",
"AddTags",
"RemoveTags",
# command line options support infrastructure
"ARGS_MANY_INSTANCES",
"ARGS_MANY_NODES",
"ARGS_MANY_GROUPS",
"ARGS_MANY_NETWORKS",
"ARGS_MANY_FILTERS",
"ARGS_NONE",
"ARGS_ONE_INSTANCE",
"ARGS_ONE_NODE",
"ARGS_ONE_GROUP",
"ARGS_ONE_OS",
"ARGS_ONE_NETWORK",
"ARGS_ONE_FILTER",
"ArgChoice",
"ArgCommand",
"ArgFile",
"ArgGroup",
"ArgHost",
"ArgInstance",
"ArgJobId",
"ArgNetwork",
"ArgNode",
"ArgOs",
"ArgExtStorage",
"ArgFilter",
"ArgSuggest",
"ArgUnknown",
"FixHvParams",
"SplitNodeOption",
"CalculateOSNames",
"ParseFields",
] + ganeti.cli_opts.__all__ # Command line options
# Query result status for clients
(QR_NORMAL,
QR_UNKNOWN,
QR_INCOMPLETE) = range(3)
#: Maximum batch size for ChooseJob
_CHOOSE_BATCH = 25
# constants used to create InstancePolicy dictionary
TISPECS_GROUP_TYPES = {
constants.ISPECS_MIN: constants.VTYPE_INT,
constants.ISPECS_MAX: constants.VTYPE_INT,
}
TISPECS_CLUSTER_TYPES = {
constants.ISPECS_MIN: constants.VTYPE_INT,
constants.ISPECS_MAX: constants.VTYPE_INT,
constants.ISPECS_STD: constants.VTYPE_INT,
}
#: User-friendly names for query2 field types
_QFT_NAMES = {
constants.QFT_UNKNOWN: "Unknown",
constants.QFT_TEXT: "Text",
constants.QFT_BOOL: "Boolean",
constants.QFT_NUMBER: "Number",
constants.QFT_NUMBER_FLOAT: "Floating-point number",
constants.QFT_UNIT: "Storage size",
constants.QFT_TIMESTAMP: "Timestamp",
constants.QFT_OTHER: "Custom",
}
class _Argument(object):
def __init__(self, min=0, max=None): # pylint: disable=W0622
self.min = min
self.max = max
def __repr__(self):
return ("<%s min=%s max=%s>" %
(self.__class__.__name__, self.min, self.max))
class ArgSuggest(_Argument):
"""Suggesting argument.
Value can be any of the ones passed to the constructor.
"""
# pylint: disable=W0622
def __init__(self, min=0, max=None, choices=None):
_Argument.__init__(self, min=min, max=max)
self.choices = choices
def __repr__(self):
return ("<%s min=%s max=%s choices=%r>" %
(self.__class__.__name__, self.min, self.max, self.choices))
class ArgChoice(ArgSuggest):
"""Choice argument.
Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
but value must be one of the choices.
"""
class ArgUnknown(_Argument):
"""Unknown argument to program (e.g. determined at runtime).
"""
class ArgInstance(_Argument):
"""Instances argument.
"""
class ArgNode(_Argument):
"""Node argument.
"""
class ArgNetwork(_Argument):
"""Network argument.
"""
class ArgGroup(_Argument):
"""Node group argument.
"""
class ArgJobId(_Argument):
"""Job ID argument.
"""
class ArgFile(_Argument):
"""File path argument.
"""
class ArgCommand(_Argument):
"""Command argument.
"""
class ArgHost(_Argument):
"""Host argument.
"""
class ArgOs(_Argument):
"""OS argument.
"""
class ArgExtStorage(_Argument):
"""ExtStorage argument.
"""
class ArgFilter(_Argument):
"""Filter UUID argument.
"""
ARGS_NONE = []
ARGS_MANY_INSTANCES = [ArgInstance()]
ARGS_MANY_NETWORKS = [ArgNetwork()]
ARGS_MANY_NODES = [ArgNode()]
ARGS_MANY_GROUPS = [ArgGroup()]
ARGS_MANY_FILTERS = [ArgFilter()]
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
ARGS_ONE_NETWORK = [ArgNetwork(min=1, max=1)]
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
ARGS_ONE_GROUP = [ArgGroup(min=1, max=1)]
ARGS_ONE_OS = [ArgOs(min=1, max=1)]
ARGS_ONE_FILTER = [ArgFilter(min=1, max=1)]
def _ExtractTagsObject(opts, args):
"""Extract the tag type object.
Note that this function will modify its args parameter.
"""
if not hasattr(opts, "tag_type"):
raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
kind = opts.tag_type
if kind == constants.TAG_CLUSTER:
retval = kind, ""
elif kind in (constants.TAG_NODEGROUP,
constants.TAG_NODE,
constants.TAG_NETWORK,
constants.TAG_INSTANCE):
if not args:
raise errors.OpPrereqError("no arguments passed to the command",
errors.ECODE_INVAL)
name = args.pop(0)
retval = kind, name
else:
raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
return retval
def _ExtendTags(opts, args):
"""Extend the args if a source file has been given.
This function will extend the tags with the contents of the file
passed in the 'tags_source' attribute of the opts parameter. A file
named '-' will be replaced by stdin.
"""
fname = opts.tags_source
if fname is None:
return
if fname == "-":
new_fh = sys.stdin
else:
new_fh = open(fname, "r")
new_data = []
try:
# we don't use the nice 'new_data = [line.strip() for line in fh]'
# because of python bug 1633941
while True:
line = new_fh.readline()
if not line:
break
new_data.append(line.strip())
finally:
new_fh.close()
args.extend(new_data)
def ListTags(opts, args):
"""List the tags on a given object.
This is a generic implementation that knows how to deal with all
three cases of tag objects (cluster, node, instance). The opts
argument is expected to contain a tag_type field denoting what
object type we work on.
"""
kind, name = _ExtractTagsObject(opts, args)
cl = GetClient()
result = cl.QueryTags(kind, name)
result = list(result)
result.sort()
for tag in result:
ToStdout(tag)
def AddTags(opts, args):
"""Add tags on a given object.
This is a generic implementation that knows how to deal with all
three cases of tag objects (cluster, node, instance). The opts
argument is expected to contain a tag_type field denoting what
object type we work on.
"""
kind, name = _ExtractTagsObject(opts, args)
_ExtendTags(opts, args)
if not args:
raise errors.OpPrereqError("No tags to be added", errors.ECODE_INVAL)
op = opcodes.OpTagsSet(kind=kind, name=name, tags=args)
SubmitOrSend(op, opts)
def RemoveTags(opts, args):
"""Remove tags from a given object.
This is a generic implementation that knows how to deal with all
three cases of tag objects (cluster, node, instance). The opts
argument is expected to contain a tag_type field denoting what
object type we work on.
"""
kind, name = _ExtractTagsObject(opts, args)
_ExtendTags(opts, args)
if not args:
raise errors.OpPrereqError("No tags to be removed", errors.ECODE_INVAL)
op = opcodes.OpTagsDel(kind=kind, name=name, tags=args)
SubmitOrSend(op, opts)
class _ShowUsage(Exception):
"""Exception class for L{_ParseArgs}.
"""
def __init__(self, exit_error):
"""Initializes instances of this class.
@type exit_error: bool
@param exit_error: Whether to report failure on exit
"""
Exception.__init__(self)
self.exit_error = exit_error
class _ShowVersion(Exception):
"""Exception class for L{_ParseArgs}.
"""
def _ParseArgs(binary, argv, commands, aliases, env_override):
"""Parser for the command line arguments.
This function parses the arguments and returns the function which
must be executed together with its (modified) arguments.
@param binary: Script name
@param argv: Command line arguments
@param commands: Dictionary containing command definitions
@param aliases: dictionary with command aliases {"alias": "target", ...}
@param env_override: list of env variables allowed for default args
@raise _ShowUsage: If usage description should be shown
@raise _ShowVersion: If version should be shown
"""
assert not (env_override - set(commands))
assert not (set(aliases.keys()) & set(commands.keys()))
if len(argv) > 1:
cmd = argv[1]
else:
# No option or command given
raise _ShowUsage(exit_error=True)
if cmd == "--version":
raise _ShowVersion()
elif cmd == "--help":
raise _ShowUsage(exit_error=False)
elif not (cmd in commands or cmd in aliases):
raise _ShowUsage(exit_error=True)
# get command, unalias it, and look it up in commands
if cmd in aliases:
if aliases[cmd] not in commands:
raise errors.ProgrammerError("Alias '%s' maps to non-existing"
" command '%s'" % (cmd, aliases[cmd]))
cmd = aliases[cmd]
if cmd in env_override:
args_env_name = ("%s_%s" % (binary.replace("-", "_"), cmd)).upper()
env_args = os.environ.get(args_env_name)
if env_args:
argv = utils.InsertAtPos(argv, 2, shlex.split(env_args))
func, args_def, parser_opts, usage, description = commands[cmd]
parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
description=description,
formatter=TitledHelpFormatter(),
usage="%%prog %s %s" % (cmd, usage))
parser.disable_interspersed_args()
options, args = parser.parse_args(args=argv[2:])
if not _CheckArguments(cmd, args_def, args):
return None, None, None
return func, options, args
def _FormatUsage(binary, commands):
"""Generates a nice description of all commands.
@param binary: Script name
@param commands: Dictionary containing command definitions
"""
# compute the max line length for cmd + usage
mlen = min(60, max(map(len, commands)))
yield "Usage: %s {command} [options...] [argument...]" % binary
yield "%s <command> --help to see details, or man %s" % (binary, binary)
yield ""
yield "Commands:"
# and format a nice command list
for (cmd, (_, _, _, _, help_text)) in sorted(commands.items()):
help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
yield " %-*s - %s" % (mlen, cmd, help_lines.pop(0))
for line in help_lines:
yield " %-*s %s" % (mlen, "", line)
yield ""
def _CheckArguments(cmd, args_def, args):
"""Verifies the arguments using the argument definition.
Algorithm:
1. Abort with error if values specified by user but none expected.
1. For each argument in definition
1. Keep running count of minimum number of values (min_count)
1. Keep running count of maximum number of values (max_count)
1. If it has an unlimited number of values
1. Abort with error if it's not the last argument in the definition
1. If last argument has limited number of values
1. Abort with error if number of values doesn't match or is too large
1. Abort with error if user didn't pass enough values (min_count)
"""
if args and not args_def:
ToStderr("Error: Command %s expects no arguments", cmd)
return False
min_count = None
max_count = None
check_max = None
last_idx = len(args_def) - 1
for idx, arg in enumerate(args_def):
if min_count is None:
min_count = arg.min
elif arg.min is not None:
min_count += arg.min
if max_count is None:
max_count = arg.max
elif arg.max is not None:
max_count += arg.max
if idx == last_idx:
check_max = (arg.max is not None)
elif arg.max is None:
raise errors.ProgrammerError("Only the last argument can have max=None")
if check_max:
# Command with exact number of arguments
if (min_count is not None and max_count is not None and
min_count == max_count and len(args) != min_count):
ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
return False
# Command with limited number of arguments
if max_count is not None and len(args) > max_count:
ToStderr("Error: Command %s expects only %d argument(s)",
cmd, max_count)
return False
# Command with some required arguments
if min_count is not None and len(args) < min_count:
ToStderr("Error: Command %s expects at least %d argument(s)",
cmd, min_count)
return False
return True
def SplitNodeOption(value):
"""Splits the value of a --node option.
"""
if value and ":" in value:
return value.split(":", 1)
else:
return (value, None)
def CalculateOSNames(os_name, os_variants):
"""Calculates all the names an OS can be called, according to its variants.
@type os_name: string
@param os_name: base name of the os
@type os_variants: list or None
@param os_variants: list of supported variants
@rtype: list
@return: list of valid names
"""
if os_variants:
return ["%s+%s" % (os_name, v) for v in os_variants]
else:
return [os_name]
def ParseFields(selected, default):
"""Parses the values of "--field"-like options.
@type selected: string or None
@param selected: User-selected options
@type default: list
@param default: Default fields
"""
if selected is None:
return default
if selected.startswith("+"):
return default + selected[1:].split(",")
return selected.split(",")
def AskUser(text, choices=None):
"""Ask the user a question.
@param text: the question to ask
@param choices: list with elements tuples (input_char, return_value,
description); if not given, it will default to: [('y', True,
'Perform the operation'), ('n', False, 'Do no do the operation')];
note that the '?' char is reserved for help
@return: one of the return values from the choices list; if input is
not possible (i.e. not running with a tty, we return the last
entry from the list
"""
if choices is None:
choices = [("y", True, "Perform the operation"),
("n", False, "Do not perform the operation")]
if not choices or not isinstance(choices, list):
raise errors.ProgrammerError("Invalid choices argument to AskUser")
for entry in choices:
if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == "?":
raise errors.ProgrammerError("Invalid choices element to AskUser")
answer = choices[-1][1]
new_text = []
for line in text.splitlines():
new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
text = "\n".join(new_text)
try:
f = file("/dev/tty", "a+")
except IOError:
return answer
try:
chars = [entry[0] for entry in choices]
chars[-1] = "[%s]" % chars[-1]
chars.append("?")
maps = dict([(entry[0], entry[1]) for entry in choices])
while True:
f.write(text)
f.write("\n")
f.write("/".join(chars))
f.write(": ")
line = f.readline(2).strip().lower()
if line in maps:
answer = maps[line]
break
elif line == "?":
for entry in choices:
f.write(" %s - %s\n" % (entry[0], entry[2]))
f.write("\n")
continue
finally:
f.close()
return answer
def SendJob(ops, cl=None):
"""Function to submit an opcode without waiting for the results.
@type ops: list
@param ops: list of opcodes
@type cl: luxi.Client
@param cl: the luxi client to use for communicating with the master;
if None, a new client will be created
"""
if cl is None:
cl = GetClient()
job_id = cl.SubmitJob(ops)
return job_id
def GenericPollJob(job_id, cbs, report_cbs, cancel_fn=None,
update_freq=constants.DEFAULT_WFJC_TIMEOUT):
"""Generic job-polling function.
@type job_id: number
@param job_id: Job ID
@type cbs: Instance of L{JobPollCbBase}
@param cbs: Data callbacks
@type report_cbs: Instance of L{JobPollReportCbBase}
@param report_cbs: Reporting callbacks
@type cancel_fn: Function returning a boolean
@param cancel_fn: Function to check if we should cancel the running job
@type update_freq: int/long
@param update_freq: number of seconds between each WFJC reports
@return: the opresult of the job
@raise errors.JobLost: If job can't be found
@raise errors.JobCanceled: If job is canceled
@raise errors.OpExecError: If job didn't succeed
"""
prev_job_info = None
prev_logmsg_serial = None
status = None
should_cancel = False
if update_freq <= 0:
raise errors.ParameterError("Update frequency must be a positive number")
while True:
if cancel_fn:
timer = 0
while timer < update_freq:
result = cbs.WaitForJobChangeOnce(job_id, ["status"], prev_job_info,
prev_logmsg_serial,
timeout=constants.CLI_WFJC_FREQUENCY)
should_cancel = cancel_fn()
if should_cancel or not result or result != constants.JOB_NOTCHANGED:
break
timer += constants.CLI_WFJC_FREQUENCY
else:
result = cbs.WaitForJobChangeOnce(job_id, ["status"], prev_job_info,
prev_logmsg_serial, timeout=update_freq)
if not result:
# job not found, go away!
raise errors.JobLost("Job with id %s lost" % job_id)
if should_cancel:
logging.info("Job %s canceled because the client timed out.", job_id)
cbs.CancelJob(job_id)
raise errors.JobCanceled("Job was canceled")
if result == constants.JOB_NOTCHANGED:
report_cbs.ReportNotChanged(job_id, status)
# Wait again
continue
# Split result, a tuple of (field values, log entries)
(job_info, log_entries) = result
(status, ) = job_info
if log_entries:
for log_entry in log_entries:
(serial, timestamp, log_type, message) = log_entry
report_cbs.ReportLogMessage(job_id, serial, timestamp,
log_type, message)
prev_logmsg_serial = max(prev_logmsg_serial, serial)
# TODO: Handle canceled and archived jobs
elif status in (constants.JOB_STATUS_SUCCESS,
constants.JOB_STATUS_ERROR,
constants.JOB_STATUS_CANCELING,
constants.JOB_STATUS_CANCELED):
break
prev_job_info = job_info
jobs = cbs.QueryJobs([job_id], ["status", "opstatus", "opresult"])
if not jobs:
raise errors.JobLost("Job with id %s lost" % job_id)
status, opstatus, result = jobs[0]
if status == constants.JOB_STATUS_SUCCESS:
return result
if status in (constants.JOB_STATUS_CANCELING, constants.JOB_STATUS_CANCELED):
raise errors.JobCanceled("Job was canceled")
has_ok = False
for idx, (status, msg) in enumerate(zip(opstatus, result)):
if status == constants.OP_STATUS_SUCCESS:
has_ok = True
elif status == constants.OP_STATUS_ERROR:
errors.MaybeRaise(msg)
if has_ok:
raise errors.OpExecError("partial failure (opcode %d): %s" %
(idx, msg))
raise errors.OpExecError(str(msg))
# default failure mode
raise errors.OpExecError(result)
class JobPollCbBase(object):
"""Base class for L{GenericPollJob} callbacks.
"""
def __init__(self):
"""Initializes this class.
"""
def WaitForJobChangeOnce(self, job_id, fields,
prev_job_info, prev_log_serial,
timeout=constants.DEFAULT_WFJC_TIMEOUT):
"""Waits for changes on a job.
"""
raise NotImplementedError()
def QueryJobs(self, job_ids, fields):
"""Returns the selected fields for the selected job IDs.
@type job_ids: list of numbers
@param job_ids: Job IDs
@type fields: list of strings
@param fields: Fields
"""
raise NotImplementedError()
def CancelJob(self, job_id):
"""Cancels a currently running job.
@type job_id: number
@param job_id: The ID of the Job we want to cancel
"""
raise NotImplementedError()
class JobPollReportCbBase(object):
"""Base class for L{GenericPollJob} reporting callbacks.
"""
def __init__(self):
"""Initializes this class.
"""
def ReportLogMessage(self, job_id, serial, timestamp, log_type, log_msg):
"""Handles a log message.
"""
raise NotImplementedError()
def ReportNotChanged(self, job_id, status):
"""Called for if a job hasn't changed in a while.
@type job_id: number
@param job_id: Job ID
@type status: string or None
@param status: Job status if available
"""
raise NotImplementedError()
class _LuxiJobPollCb(JobPollCbBase):
def __init__(self, cl):
"""Initializes this class.
"""
JobPollCbBase.__init__(self)
self.cl = cl
def WaitForJobChangeOnce(self, job_id, fields,
prev_job_info, prev_log_serial,
timeout=constants.DEFAULT_WFJC_TIMEOUT):
"""Waits for changes on a job.
"""
return self.cl.WaitForJobChangeOnce(job_id, fields,
prev_job_info, prev_log_serial,
timeout=timeout)
def QueryJobs(self, job_ids, fields):
"""Returns the selected fields for the selected job IDs.
"""
return self.cl.QueryJobs(job_ids, fields)
def CancelJob(self, job_id):
"""Cancels a currently running job.
"""
return self.cl.CancelJob(job_id)
class FeedbackFnJobPollReportCb(JobPollReportCbBase):
def __init__(self, feedback_fn):
"""Initializes this class.
"""
JobPollReportCbBase.__init__(self)
self.feedback_fn = feedback_fn
assert callable(feedback_fn)
def ReportLogMessage(self, job_id, serial, timestamp, log_type, log_msg):
"""Handles a log message.
"""
self.feedback_fn((timestamp, log_type, log_msg))
def ReportNotChanged(self, job_id, status):
"""Called if a job hasn't changed in a while.
"""
# Ignore
class StdioJobPollReportCb(JobPollReportCbBase):
def __init__(self):
"""Initializes this class.
"""
JobPollReportCbBase.__init__(self)
self.notified_queued = False
self.notified_waitlock = False
def ReportLogMessage(self, job_id, serial, timestamp, log_type, log_msg):
"""Handles a log message.
"""
ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)),
FormatLogMessage(log_type, log_msg))
def ReportNotChanged(self, job_id, status):
"""Called if a job hasn't changed in a while.
"""
if status is None:
return
if status == constants.JOB_STATUS_QUEUED and not self.notified_queued:
ToStderr("Job %s is waiting in queue", job_id)
self.notified_queued = True
elif status == constants.JOB_STATUS_WAITING and not self.notified_waitlock:
ToStderr("Job %s is trying to acquire all necessary locks", job_id)
self.notified_waitlock = True
def FormatLogMessage(log_type, log_msg):
"""Formats a job message according to its type.
"""
if log_type != constants.ELOG_MESSAGE:
log_msg = str(log_msg)
return utils.SafeEncode(log_msg)
def PollJob(job_id, cl=None, feedback_fn=None, reporter=None, cancel_fn=None,
update_freq=constants.DEFAULT_WFJC_TIMEOUT):
"""Function to poll for the result of a job.
@type job_id: job identified
@param job_id: the job to poll for results
@type cl: luxi.Client
@param cl: the luxi client to use for communicating with the master;
if None, a new client will be created
@type cancel_fn: Function returning a boolean
@param cancel_fn: Function to check if we should cancel the running job
@type update_freq: int/long
@param update_freq: number of seconds between each WFJC report
"""
if cl is None:
cl = GetClient()
if reporter is None:
if feedback_fn:
reporter = FeedbackFnJobPollReportCb(feedback_fn)
else:
reporter = StdioJobPollReportCb()
elif feedback_fn:
raise errors.ProgrammerError("Can't specify reporter and feedback function")
return GenericPollJob(job_id, _LuxiJobPollCb(cl), reporter,
cancel_fn=cancel_fn, update_freq=update_freq)
def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None, reporter=None):
"""Legacy function to submit an opcode.
This is just a simple wrapper over the construction of the processor
instance. It should be extended to better handle feedback and
interaction functions.
"""
if cl is None:
cl = GetClient()
SetGenericOpcodeOpts([op], opts)
job_id = SendJob([op], cl=cl)
if hasattr(opts, "print_jobid") and opts.print_jobid:
ToStdout("%d" % job_id)
op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn,
reporter=reporter)
return op_results[0]
def SubmitOpCodeToDrainedQueue(op):
"""Forcefully insert a job in the queue, even if it is drained.
"""
cl = GetClient()
job_id = cl.SubmitJobToDrainedQueue([op])
op_results = PollJob(job_id, cl=cl)
return op_results[0]
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
"""Wrapper around SubmitOpCode or SendJob.
This function will decide, based on the 'opts' parameter, whether to
submit and wait for the result of the opcode (and return it), or
whether to just send the job and print its identifier. It is used in
order to simplify the implementation of the '--submit' option.
It will also process the opcodes if we're sending the via SendJob
(otherwise SubmitOpCode does it).
"""
if opts and opts.submit_only:
job = [op]
SetGenericOpcodeOpts(job, opts)
job_id = SendJob(job, cl=cl)
if opts.print_jobid:
ToStdout("%d" % job_id)
raise errors.JobSubmittedException(job_id)
else:
return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn, opts=opts)
def _InitReasonTrail(op, opts):
"""Builds the first part of the reason trail
Builds the initial part of the reason trail, adding the user provided reason
(if it exists) and the name of the command starting the operation.
@param op: the opcode the reason trail will be added to
@param opts: the command line options selected by the user
"""
assert len(sys.argv) >= 2
trail = []
if opts.reason:
trail.append((constants.OPCODE_REASON_SRC_USER,
opts.reason,
utils.EpochNano()))
binary = os.path.basename(sys.argv[0])
source = "%s:%s" % (constants.OPCODE_REASON_SRC_CLIENT, binary)
command = sys.argv[1]
trail.append((source, command, utils.EpochNano()))
op.reason = trail
def SetGenericOpcodeOpts(opcode_list, options):
"""Processor for generic options.
This function updates the given opcodes based on generic command
line options (like debug, dry-run, etc.).
@param opcode_list: list of opcodes
@param options: command line options or None
@return: None (in-place modification)
"""
if not options:
return
for op in opcode_list:
op.debug_level = options.debug
if hasattr(options, "dry_run"):
op.dry_run = options.dry_run
if getattr(options, "priority", None) is not None:
op.priority = options.priority
_InitReasonTrail(op, options)
def FormatError(err):
"""Return a formatted error message for a given error.
This function takes an exception instance and returns a tuple
consisting of two values: first, the recommended exit code, and
second, a string describing the error message (not
newline-terminated).
"""
retcode = 1
obuf = StringIO()
msg = str(err)
if isinstance(err, errors.ConfigurationError):
txt = "Corrupt configuration file: %s" % msg
logging.error(txt)
obuf.write(txt + "\n")
obuf.write("Aborting.")
retcode = 2
elif isinstance(err, errors.HooksAbort):
obuf.write("Failure: hooks execution failed:\n")
for node, script, out in err.args[0]:
if out:
obuf.write(" node: %s, script: %s, output: %s\n" %
(node, script, out))
else:
obuf.write(" node: %s, script: %s (no output)\n" %
(node, script))
elif isinstance(err, errors.HooksFailure):
obuf.write("Failure: hooks general failure: %s" % msg)
elif isinstance(err, errors.ResolverError):
this_host = netutils.Hostname.GetSysName()
if err.args[0] == this_host:
msg = "Failure: can't resolve my own hostname ('%s')"
else:
msg = "Failure: can't resolve hostname '%s'"
obuf.write(msg % err.args[0])
elif isinstance(err, errors.OpPrereqError):
if len(err.args) == 2:
obuf.write("Failure: prerequisites not met for this"
" operation:\nerror type: %s, error details:\n%s" %
(err.args[1], err.args[0]))
else:
obuf.write("Failure: prerequisites not met for this"
" operation:\n%s" % msg)
elif isinstance(err, errors.OpExecError):
obuf.write("Failure: command execution error:\n%s" % msg)
elif isinstance(err, errors.TagError):
obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
elif isinstance(err, errors.JobQueueDrainError):
obuf.write("Failure: the job queue is marked for drain and doesn't"
" accept new requests\n")
elif isinstance(err, errors.JobQueueFull):
obuf.write("Failure: the job queue is full and doesn't accept new"
" job submissions until old jobs are archived\n")
elif isinstance(err, errors.TypeEnforcementError):
obuf.write("Parameter Error: %s" % msg)
elif isinstance(err, errors.ParameterError):
obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
elif isinstance(err, rpcerr.NoMasterError):
if err.args[0] == pathutils.MASTER_SOCKET:
daemon = "the master daemon"
elif err.args[0] == pathutils.QUERY_SOCKET:
daemon = "the config daemon"
else:
daemon = "socket '%s'" % str(err.args[0])
obuf.write("Cannot communicate with %s.\nIs the process running"
" and listening for connections?" % daemon)
elif isinstance(err, rpcerr.TimeoutError):
obuf.write("Timeout while talking to the master daemon. Jobs might have"
" been submitted and will continue to run even if the call"
" timed out. Useful commands in this situation are \"gnt-job"
" list\", \"gnt-job cancel\" and \"gnt-job watch\". Error:\n")
obuf.write(msg)
elif isinstance(err, rpcerr.PermissionError):
obuf.write("It seems you don't have permissions to connect to the"
" master daemon.\nPlease retry as a different user.")
elif isinstance(err, rpcerr.ProtocolError):
obuf.write("Unhandled protocol error while talking to the master daemon:\n"
"%s" % msg)
elif isinstance(err, errors.JobLost):
obuf.write("Error checking job status: %s" % msg)
elif isinstance(err, errors.QueryFilterParseError):
obuf.write("Error while parsing query filter: %s\n" % err.args[0])
obuf.write("\n".join(err.GetDetails()))
elif isinstance(err, errors.GenericError):
obuf.write("Unhandled Ganeti error: %s" % msg)
elif isinstance(err, errors.JobSubmittedException):
obuf.write("JobID: %s\n" % err.args[0])
retcode = 0
else:
obuf.write("Unhandled exception: %s" % msg)
return retcode, obuf.getvalue().rstrip("\n")
def GenericMain(commands, override=None, aliases=None,
env_override=frozenset()):
"""Generic main function for all the gnt-* commands.
@param commands: a dictionary with a special structure, see the design doc
for command line handling.
@param override: if not None, we expect a dictionary with keys that will
override command line options; this can be used to pass
options from the scripts to generic functions
@param aliases: dictionary with command aliases {'alias': 'target, ...}
@param env_override: list of environment names which are allowed to submit
default args for commands
"""
# save the program name and the entire command line for later logging
if sys.argv:
binary = os.path.basename(sys.argv[0])
if not binary:
binary = sys.argv[0]
if len(sys.argv) >= 2:
logname = utils.ShellQuoteArgs([binary, sys.argv[1]])
else:
logname = binary
cmdline = utils.ShellQuoteArgs([binary] + sys.argv[1:])
else:
binary = "<unknown program>"
cmdline = "<unknown>"
if aliases is None:
aliases = {}
try:
(func, options, args) = _ParseArgs(binary, sys.argv, commands, aliases,
env_override)
except _ShowVersion:
ToStdout("%s (ganeti %s) %s", binary, constants.VCS_VERSION,
constants.RELEASE_VERSION)
return constants.EXIT_SUCCESS
except _ShowUsage, err:
for line in _FormatUsage(binary, commands):
ToStdout(line)
if err.exit_error:
return constants.EXIT_FAILURE
else:
return constants.EXIT_SUCCESS
except errors.ParameterError, err:
result, err_msg = FormatError(err)
ToStderr(err_msg)
return 1
if func is None: # parse error
return 1
if override is not None:
for key, val in override.iteritems():
setattr(options, key, val)
utils.SetupLogging(pathutils.LOG_COMMANDS, logname, debug=options.debug,
stderr_logging=True)
logging.debug("Command line: %s", cmdline)
try:
result = func(options, args)
except (errors.GenericError, rpcerr.ProtocolError,
errors.JobSubmittedException), err:
result, err_msg = FormatError(err)
logging.exception("Error during command processing")
ToStderr(err_msg)
except KeyboardInterrupt:
result = constants.EXIT_FAILURE
ToStderr("Aborted. Note that if the operation created any jobs, they"
" might have been submitted and"
" will continue to run in the background.")
except IOError, err:
if err.errno == errno.EPIPE:
# our terminal went away, we'll exit
sys.exit(constants.EXIT_FAILURE)
else:
raise
return result
def ParseNicOption(optvalue):
"""Parses the value of the --net option(s).
"""
try:
nic_max = max(int(nidx[0]) + 1 for nidx in optvalue)
except (TypeError, ValueError), err:
raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err),
errors.ECODE_INVAL)
nics = [{}] * nic_max
for nidx, ndict in optvalue:
nidx = int(nidx)
if not isinstance(ndict, dict):
raise errors.OpPrereqError("Invalid nic/%d value: expected dict,"
" got %s" % (nidx, ndict), errors.ECODE_INVAL)
utils.ForceDictType(ndict, constants.INIC_PARAMS_TYPES)
nics[nidx] = ndict
return nics
def FixHvParams(hvparams):
# In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
# comma to space because commas cannot be accepted on the command line
# (they already act as the separator between different hvparams). Still,
# RAPI should be able to accept commas for backwards compatibility.
# Therefore, we convert spaces into commas here, and we keep the old
# parsing logic everywhere else.
try:
new_usb_devices = hvparams[constants.HV_USB_DEVICES].replace(" ", ",")
hvparams[constants.HV_USB_DEVICES] = new_usb_devices
except KeyError:
#No usb_devices, no modification required
pass
def GenericInstanceCreate(mode, opts, args):
"""Add an instance to the cluster via either creation or import.
@param mode: constants.INSTANCE_CREATE or constants.INSTANCE_IMPORT
@param opts: the command line options selected by the user
@type args: list
@param args: should contain only one element, the new instance name
@rtype: int
@return: the desired exit code
"""
instance = args[0]
forthcoming = opts.ensure_value("forthcoming", False)
commit = opts.ensure_value("commit", False)
if forthcoming and commit:
raise errors.OpPrereqError("Creating an instance only forthcoming and"
" commiting it are mutally exclusive",
errors.ECODE_INVAL)
(pnode, snode) = SplitNodeOption(opts.node)
hypervisor = None
hvparams = {}
if opts.hypervisor:
hypervisor, hvparams = opts.hypervisor
if opts.nics:
nics = ParseNicOption(opts.nics)
elif opts.no_nics:
# no nics
nics = []
elif mode == constants.INSTANCE_CREATE:
# default of one nic, all auto
nics = [{}]
else:
# mode == import
nics = []
if opts.disk_template == constants.DT_DISKLESS:
if opts.disks or opts.sd_size is not None:
raise errors.OpPrereqError("Diskless instance but disk"
" information passed", errors.ECODE_INVAL)
disks = []
else:
if (not opts.disks and not opts.sd_size
and mode == constants.INSTANCE_CREATE):
raise errors.OpPrereqError("No disk information specified",
errors.ECODE_INVAL)
if opts.disks and opts.sd_size is not None:
raise errors.OpPrereqError("Please use either the '--disk' or"
" '-s' option", errors.ECODE_INVAL)
if opts.sd_size is not None:
opts.disks = [(0, {constants.IDISK_SIZE: opts.sd_size})]
if opts.disks:
try:
disk_max = max(int(didx[0]) + 1 for didx in opts.disks)
except ValueError, err:
raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
errors.ECODE_INVAL)
disks = [{}] * disk_max
else:
disks = []
for didx, ddict in opts.disks:
didx = int(didx)
if not isinstance(ddict, dict):
msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
elif constants.IDISK_SIZE in ddict:
if constants.IDISK_ADOPT in ddict:
raise errors.OpPrereqError("Only one of 'size' and 'adopt' allowed"
" (disk %d)" % didx, errors.ECODE_INVAL)
try:
ddict[constants.IDISK_SIZE] = \
utils.ParseUnit(ddict[constants.IDISK_SIZE])
except ValueError, err:
raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
(didx, err), errors.ECODE_INVAL)
elif constants.IDISK_ADOPT in ddict:
if constants.IDISK_SPINDLES in ddict:
raise errors.OpPrereqError("spindles is not a valid option when"
" adopting a disk", errors.ECODE_INVAL)
if mode == constants.INSTANCE_IMPORT:
raise errors.OpPrereqError("Disk adoption not allowed for instance"
" import", errors.ECODE_INVAL)
ddict[constants.IDISK_SIZE] = 0
else:
raise errors.OpPrereqError("Missing size or adoption source for"
" disk %d" % didx, errors.ECODE_INVAL)
if constants.IDISK_SPINDLES in ddict:
ddict[constants.IDISK_SPINDLES] = int(ddict[constants.IDISK_SPINDLES])
disks[didx] = ddict
if opts.tags is not None:
tags = opts.tags.split(",")
else:
tags = []
utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT)
utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
FixHvParams(hvparams)
osparams_private = opts.osparams_private or serializer.PrivateDict()
osparams_secret = opts.osparams_secret or serializer.PrivateDict()
helper_startup_timeout = opts.helper_startup_timeout
helper_shutdown_timeout = opts.helper_shutdown_timeout
if mode == constants.INSTANCE_CREATE:
start = opts.start
os_type = opts.os
force_variant = opts.force_variant
src_node = None
src_path = None
no_install = opts.no_install
identify_defaults = False
compress = constants.IEC_NONE
if opts.instance_communication is None:
instance_communication = False
else:
instance_communication = opts.instance_communication
elif mode == constants.INSTANCE_IMPORT:
if forthcoming:
raise errors.OpPrereqError("forthcoming instances can only be created,"
" not imported")
start = False
os_type = None
force_variant = False
src_node = opts.src_node
src_path = opts.src_dir
no_install = None
identify_defaults = opts.identify_defaults
compress = opts.compress
instance_communication = False
else:
raise errors.ProgrammerError("Invalid creation mode %s" % mode)
op = opcodes.OpInstanceCreate(
forthcoming=forthcoming,
commit=commit,
instance_name=instance,
disks=disks,
disk_template=opts.disk_template,
group_name=opts.nodegroup,
nics=nics,
conflicts_check=opts.conflicts_check,
pnode=pnode, snode=snode,
ip_check=opts.ip_check,
name_check=opts.name_check,
wait_for_sync=opts.wait_for_sync,
file_storage_dir=opts.file_storage_dir,
file_driver=opts.file_driver,
iallocator=opts.iallocator,
hypervisor=hypervisor,
hvparams=hvparams,
beparams=opts.beparams,
osparams=opts.osparams,
osparams_private=osparams_private,
osparams_secret=osparams_secret,
mode=mode,
opportunistic_locking=opts.opportunistic_locking,
start=start,
os_type=os_type,
force_variant=force_variant,
src_node=src_node,
src_path=src_path,
compress=compress,
tags=tags,
no_install=no_install,
identify_defaults=identify_defaults,
ignore_ipolicy=opts.ignore_ipolicy,
instance_communication=instance_communication,
helper_startup_timeout=helper_startup_timeout,
helper_shutdown_timeout=helper_shutdown_timeout)
SubmitOrSend(op, opts)
return 0
class _RunWhileDaemonsStoppedHelper(object):
"""Helper class for L{RunWhileDaemonsStopped} to simplify state management
"""
def __init__(self, feedback_fn, cluster_name, master_node,
online_nodes, ssh_ports, exclude_daemons, debug,
verbose):
"""Initializes this class.
@type feedback_fn: callable
@param feedback_fn: Feedback function
@type cluster_name: string
@param cluster_name: Cluster name
@type master_node: string
@param master_node Master node name
@type online_nodes: list
@param online_nodes: List of names of online nodes
@type ssh_ports: list
@param ssh_ports: List of SSH ports of online nodes
@type exclude_daemons: list of string
@param exclude_daemons: list of daemons that will be restarted on master
after all others are shutdown
@type debug: boolean
@param debug: show debug output
@type verbose: boolesn
@param verbose: show verbose output
"""
self.feedback_fn = feedback_fn
self.cluster_name = cluster_name
self.master_node = master_node
self.online_nodes = online_nodes
self.ssh_ports = dict(zip(online_nodes, ssh_ports))
self.ssh = ssh.SshRunner(self.cluster_name)
self.nonmaster_nodes = [name for name in online_nodes
if name != master_node]
self.exclude_daemons = exclude_daemons
self.debug = debug
self.verbose = verbose
assert self.master_node not in self.nonmaster_nodes
def _RunCmd(self, node_name, cmd):
"""Runs a command on the local or a remote machine.
@type node_name: string
@param node_name: Machine name
@type cmd: list
@param cmd: Command
"""
if node_name is None or node_name == self.master_node:
# No need to use SSH
result = utils.RunCmd(cmd)
else:
result = self.ssh.Run(node_name, constants.SSH_LOGIN_USER,
utils.ShellQuoteArgs(cmd),
port=self.ssh_ports[node_name])
if result.failed:
errmsg = ["Failed to run command %s" % result.cmd]
if node_name:
errmsg.append("on node %s" % node_name)
errmsg.append(": exitcode %s and error %s" %
(result.exit_code, result.output))
raise errors.OpExecError(" ".join(errmsg))
def Call(self, fn, *args):
"""Call function while all daemons are stopped.
@type fn: callable
@param fn: Function to be called
"""
# Pause watcher by acquiring an exclusive lock on watcher state file
self.feedback_fn("Blocking watcher")
watcher_block = utils.FileLock.Open(pathutils.WATCHER_LOCK_FILE)
try:
# TODO: Currently, this just blocks. There's no timeout.
# TODO: Should it be a shared lock?
watcher_block.Exclusive(blocking=True)
# Stop master daemons, so that no new jobs can come in and all running
# ones are finished
self.feedback_fn("Stopping master daemons")
self._RunCmd(None, [pathutils.DAEMON_UTIL, "stop-master"])
try:
# Stop daemons on all nodes
online_nodes = [self.master_node] + [n for n in self.online_nodes
if n != self.master_node]
for node_name in online_nodes:
self.feedback_fn("Stopping daemons on %s" % node_name)
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "stop-all"])
# Starting any daemons listed as exception
if node_name == self.master_node:
for daemon in self.exclude_daemons:
self.feedback_fn("Starting daemon '%s' on %s" % (daemon,
node_name))
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "start", daemon])
# All daemons are shut down now
try:
return fn(self, *args)
except Exception, err:
_, errmsg = FormatError(err)
logging.exception("Caught exception")
self.feedback_fn(errmsg)
raise
finally:
# Start cluster again, master node last
for node_name in self.nonmaster_nodes + [self.master_node]:
# Stopping any daemons listed as exception.
# This might look unnecessary, but it makes sure that daemon-util
# starts all daemons in the right order.
if node_name == self.master_node:
self.exclude_daemons.reverse()
for daemon in self.exclude_daemons:
self.feedback_fn("Stopping daemon '%s' on %s" % (daemon,
node_name))
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "stop", daemon])
self.feedback_fn("Starting daemons on %s" % node_name)
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "start-all"])
finally:
# Resume watcher
watcher_block.Close()
def RunWhileDaemonsStopped(feedback_fn, exclude_daemons, fn, *args, **kwargs):
"""Calls a function while all cluster daemons are stopped.
@type feedback_fn: callable
@param feedback_fn: Feedback function
@type exclude_daemons: list of string
@param exclude_daemons: list of daemons that stopped, but immediately
restarted on the master to be available when calling
'fn'. If None, all daemons will be stopped and none
will be started before calling 'fn'.
@type fn: callable
@param fn: Function to be called when daemons are stopped
"""
feedback_fn("Gathering cluster information")
# This ensures we're running on the master daemon
cl = GetClient()
(cluster_name, master_node) = \
cl.QueryConfigValues(["cluster_name", "master_node"])
online_nodes = GetOnlineNodes([], cl=cl)
ssh_ports = GetNodesSshPorts(online_nodes, cl)
# Don't keep a reference to the client. The master daemon will go away.
del cl
assert master_node in online_nodes
if exclude_daemons is None:
exclude_daemons = []
debug = kwargs.get("debug", False)
verbose = kwargs.get("verbose", False)
return _RunWhileDaemonsStoppedHelper(
feedback_fn, cluster_name, master_node, online_nodes, ssh_ports,
exclude_daemons, debug, verbose).Call(fn, *args)
def RunWhileClusterStopped(feedback_fn, fn, *args):
"""Calls a function while all cluster daemons are stopped.
@type feedback_fn: callable
@param feedback_fn: Feedback function
@type fn: callable
@param fn: Function to be called when daemons are stopped
"""
RunWhileDaemonsStopped(feedback_fn, None, fn, *args)
def GenerateTable(headers, fields, separator, data,
numfields=None, unitfields=None,
units=None):
"""Prints a table with headers and different fields.
@type headers: dict
@param headers: dictionary mapping field names to headers for
the table
@type fields: list
@param fields: the field names corresponding to each row in
the data field
@param separator: the separator to be used; if this is None,
the default 'smart' algorithm is used which computes optimal
field width, otherwise just the separator is used between
each field
@type data: list
@param data: a list of lists, each sublist being one row to be output
@type numfields: list
@param numfields: a list with the fields that hold numeric
values and thus should be right-aligned
@type unitfields: list
@param unitfields: a list with the fields that hold numeric
values that should be formatted with the units field
@type units: string or None
@param units: the units we should use for formatting, or None for
automatic choice (human-readable for non-separator usage, otherwise
megabytes); this is a one-letter string
"""
if units is None:
if separator:
units = "m"
else:
units = "h"
if numfields is None:
numfields = []
if unitfields is None:
unitfields = []
numfields = utils.FieldSet(*numfields) # pylint: disable=W0142
unitfields = utils.FieldSet(*unitfields) # pylint: disable=W0142
format_fields = []
for field in fields:
if headers and field not in headers:
# TODO: handle better unknown fields (either revert to old
# style of raising exception, or deal more intelligently with
# variable fields)
headers[field] = field
if separator is not None:
format_fields.append("%s")
elif numfields.Matches(field):
format_fields.append("%*s")
else:
format_fields.append("%-*s")
if separator is None:
mlens = [0 for name in fields]
format_str = " ".join(format_fields)
else:
format_str = separator.replace("%", "%%").join(format_fields)
for row in data:
if row is None:
continue
for idx, val in enumerate(row):
if unitfields.Matches(fields[idx]):
try:
val = int(val)
except (TypeError, ValueError):
pass
else:
val = row[idx] = utils.FormatUnit(val, units)
val = row[idx] = str(val)
if separator is None:
mlens[idx] = max(mlens[idx], len(val))
result = []
if headers:
args = []
for idx, name in enumerate(fields):
hdr = headers[name]
if separator is None:
mlens[idx] = max(mlens[idx], len(hdr))
args.append(mlens[idx])
args.append(hdr)
result.append(format_str % tuple(args))
if separator is None:
assert len(mlens) == len(fields)
if fields and not numfields.Matches(fields[-1]):
mlens[-1] = 0
for line in data:
args = []
if line is None:
line = ["-" for _ in fields]
for idx in range(len(fields)):
if separator is None:
args.append(mlens[idx])
args.append(line[idx])
result.append(format_str % tuple(args))
return result
def _FormatBool(value):
"""Formats a boolean value as a string.
"""
if value:
return "Y"
return "N"
#: Default formatting for query results; (callback, align right)
_DEFAULT_FORMAT_QUERY = {
constants.QFT_TEXT: (str, False),
constants.QFT_BOOL: (_FormatBool, False),
constants.QFT_NUMBER: (str, True),
constants.QFT_NUMBER_FLOAT: (str, True),
constants.QFT_TIMESTAMP: (utils.FormatTime, False),
constants.QFT_OTHER: (str, False),
constants.QFT_UNKNOWN: (str, False),
}
def _GetColumnFormatter(fdef, override, unit):
"""Returns formatting function for a field.
@type fdef: L{objects.QueryFieldDefinition}
@type override: dict
@param override: Dictionary for overriding field formatting functions,
indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
@type unit: string
@param unit: Unit used for formatting fields of type L{constants.QFT_UNIT}
@rtype: tuple; (callable, bool)
@return: Returns the function to format a value (takes one parameter) and a
boolean for aligning the value on the right-hand side
"""
fmt = override.get(fdef.name, None)
if fmt is not None:
return fmt
assert constants.QFT_UNIT not in _DEFAULT_FORMAT_QUERY
if fdef.kind == constants.QFT_UNIT:
# Can't keep this information in the static dictionary
return (lambda value: utils.FormatUnit(value, unit), True)
fmt = _DEFAULT_FORMAT_QUERY.get(fdef.kind, None)
if fmt is not None:
return fmt
raise NotImplementedError("Can't format column type '%s'" % fdef.kind)
class _QueryColumnFormatter(object):
"""Callable class for formatting fields of a query.
"""
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
self._verbose = verbose
def __call__(self, data):
"""Returns a field's string representation.
"""
(status, value) = data
# Report status
self._status_fn(status)
if status == constants.RS_NORMAL:
return self._fn(value)
assert value is None, \
"Found value %r for abnormal status %s" % (value, status)
return FormatResultError(status, self._verbose)
def FormatResultError(status, verbose):
"""Formats result status other than L{constants.RS_NORMAL}.
@param status: The result status
@type verbose: boolean
@param verbose: Whether to return the verbose text
@return: Text of result status
"""
assert status != constants.RS_NORMAL, \
"FormatResultError called with status equal to constants.RS_NORMAL"
try:
(verbose_text, normal_text) = constants.RSS_DESCRIPTION[status]
except KeyError:
raise NotImplementedError("Unknown status %s" % status)
else:
if verbose:
return verbose_text
return normal_text
def FormatQueryResult(result, unit=None, format_override=None, separator=None,
header=False, verbose=False):
"""Formats data in L{objects.QueryResponse}.
@type result: L{objects.QueryResponse}
@param result: result of query operation
@type unit: string
@param unit: Unit used for formatting fields of type L{constants.QFT_UNIT},
see L{utils.text.FormatUnit}
@type format_override: dict
@param format_override: Dictionary for overriding field formatting functions,
indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
@type separator: string or 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:
if separator:
unit = "m"
else:
unit = "h"
if format_override is None:
format_override = {}
stats = dict.fromkeys(constants.RS_ALL, 0)
def _RecordStatus(status):
if status in stats:
stats[status] += 1
columns = []
for fdef in result.fields:
assert fdef.title and fdef.name
(fn, align_right) = _GetColumnFormatter(fdef, format_override, unit)
columns.append(TableColumn(fdef.title,
_QueryColumnFormatter(fn, _RecordStatus,
verbose),
align_right))
table = FormatTable(result.data, columns, header, separator)
# Collect statistics
assert len(stats) == len(constants.RS_ALL)
assert compat.all(count >= 0 for count in stats.values())
# Determine overall status. If there was no data, unknown fields must be
# detected via the field definitions.
if (stats[constants.RS_UNKNOWN] or
(not result.data and _GetUnknownFields(result.fields))):
status = QR_UNKNOWN
elif compat.any(count > 0 for key, count in stats.items()
if key != constants.RS_NORMAL):
status = QR_INCOMPLETE
else:
status = QR_NORMAL
return (status, table)
def _GetUnknownFields(fdefs):
"""Returns list of unknown fields included in C{fdefs}.
@type fdefs: list of L{objects.QueryFieldDefinition}
"""
return [fdef for fdef in fdefs
if fdef.kind == constants.QFT_UNKNOWN]
def _WarnUnknownFields(fdefs):
"""Prints a warning to stderr if a query included unknown fields.
@type fdefs: list of L{objects.QueryFieldDefinition}
"""
unknown = _GetUnknownFields(fdefs)
if unknown:
ToStderr("Warning: Queried for unknown fields %s",
utils.CommaJoin(fdef.name for fdef in unknown))
return True
return False
def GenericList(resource, fields, names, unit, separator, header, cl=None,
format_override=None, verbose=False, force_filter=False,
namefield=None, qfilter=None, isnumeric=False):
"""Generic implementation for listing all items of a resource.
@param resource: One of L{constants.QR_VIA_LUXI}
@type fields: list of strings
@param fields: List of fields to query for
@type names: list of strings
@param names: Names of items to query for
@type unit: string or None
@param unit: Unit used for formatting fields of type L{constants.QFT_UNIT} or
None for automatic choice (human-readable for non-separator usage,
otherwise megabytes); this is a one-letter string
@type separator: string or None
@param separator: String used to separate fields
@type header: bool
@param header: Whether to show header row
@type force_filter: bool
@param force_filter: Whether to always treat names as filter
@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
@type namefield: string
@param namefield: Name of field to use for simple filters (see
L{qlang.MakeFilter} for details)
@type qfilter: list or None
@param qfilter: Query filter (in addition to names)
@param isnumeric: bool
@param isnumeric: Whether the namefield's type is numeric, and therefore
any simple filters built by namefield should use integer values to
reflect that
"""
if not names:
names = None
namefilter = qlang.MakeFilter(names, force_filter, namefield=namefield,
isnumeric=isnumeric)
if qfilter is None:
qfilter = namefilter
elif namefilter is not None:
qfilter = [qlang.OP_AND, namefilter, qfilter]
if cl is None:
cl = GetClient()
response = cl.Query(resource, fields, qfilter)
found_unknown = _WarnUnknownFields(response.fields)
(status, data) = FormatQueryResult(response, unit=unit, separator=separator,
header=header,
format_override=format_override,
verbose=verbose)
for line in data:
ToStdout(line)
assert ((found_unknown and status == QR_UNKNOWN) or
(not found_unknown and status != QR_UNKNOWN))
if status == QR_UNKNOWN:
return constants.EXIT_UNKNOWN_FIELD
# TODO: Should the list command fail if not all data could be collected?
return constants.EXIT_SUCCESS
def _FieldDescValues(fdef):
"""Helper function for L{GenericListFields} to get query field description.
@type fdef: L{objects.QueryFieldDefinition}
@rtype: list
"""
return [
fdef.name,
_QFT_NAMES.get(fdef.kind, fdef.kind),
fdef.title,
fdef.doc,
]
def GenericListFields(resource, fields, separator, header, cl=None):
"""Generic implementation for listing fields for a resource.
@param resource: One of L{constants.QR_VIA_LUXI}
@type fields: list of strings
@param fields: List of fields to query for
@type separator: string or None
@param separator: String used to separate fields
@type header: bool
@param header: Whether to show header row
"""
if cl is None:
cl = GetClient()
if not fields:
fields = None
response = cl.QueryFields(resource, fields)
found_unknown = _WarnUnknownFields(response.fields)
columns = [
TableColumn("Name", str, False),
TableColumn("Type", str, False),
TableColumn("Title", str, False),
TableColumn("Description", str, False),
]
rows = map(_FieldDescValues, response.fields)
for line in FormatTable(rows, columns, header, separator):
ToStdout(line)
if found_unknown:
return constants.EXIT_UNKNOWN_FIELD
return constants.EXIT_SUCCESS
class TableColumn(object):
"""Describes a column for L{FormatTable}.
"""
def __init__(self, title, fn, align_right):
"""Initializes this class.
@type title: string
@param title: Column title
@type fn: callable
@param fn: Formatting function
@type align_right: bool
@param align_right: Whether to align values on the right-hand side
"""
self.title = title
self.format = fn
self.align_right = align_right
def _GetColFormatString(width, align_right):
"""Returns the format string for a field.
"""
if align_right:
sign = ""
else:
sign = "-"
return "%%%s%ss" % (sign, width)
def FormatTable(rows, columns, header, separator):
"""Formats data as a table.
@type rows: list of lists
@param rows: Row data, one list per row
@type columns: list of L{TableColumn}
@param columns: Column descriptions
@type header: bool
@param header: Whether to show header row
@type separator: string or None
@param separator: String used to separate columns
"""
if header:
data = [[col.title for col in columns]]
colwidth = [len(col.title) for col in columns]
else:
data = []
colwidth = [0 for _ in columns]
# Format row data
for row in rows:
assert len(row) == len(columns)
formatted = [col.format(value) for value, col in zip(row, columns)]
if separator is None:
# Update column widths
for idx, (oldwidth, value) in enumerate(zip(colwidth, formatted)):
# Modifying a list's items while iterating is fine
colwidth[idx] = max(oldwidth, len(value))
data.append(formatted)
if separator is not None:
# Return early if a separator is used
return [separator.join(row) for row in data]
if columns and not columns[-1].align_right:
# Avoid unnecessary spaces at end of line
colwidth[-1] = 0
# Build format string
fmt = " ".join([_GetColFormatString(width, col.align_right)
for col, width in zip(columns, colwidth)])
return [fmt % tuple(row) for row in data]
def FormatTimestamp(ts):
"""Formats a given timestamp.
@type ts: timestamp
@param ts: a timeval-type timestamp, a tuple of seconds and microseconds
@rtype: string
@return: a string with the formatted timestamp
"""
if not isinstance(ts, (tuple, list)) or len(ts) != 2:
return "?"
(sec, usecs) = ts
return utils.FormatTime(sec, usecs=usecs)
def ParseTimespec(value):
"""Parse a time specification.
The following suffixed will be recognized:
- s: seconds
- m: minutes
- h: hours
- d: day
- w: weeks
Without any suffix, the value will be taken to be in seconds.
"""
value = str(value)
if not value:
raise errors.OpPrereqError("Empty time specification passed",
errors.ECODE_INVAL)
suffix_map = {
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
"w": 604800,
}
if value[-1] not in suffix_map:
try:
value = int(value)
except (TypeError, ValueError):
raise errors.OpPrereqError("Invalid time specification '%s'" % value,
errors.ECODE_INVAL)
else:
multiplier = suffix_map[value[-1]]
value = value[:-1]
if not value: # no data left after stripping the suffix
raise errors.OpPrereqError("Invalid time specification (only"
" suffix passed)", errors.ECODE_INVAL)
try:
value = int(value) * multiplier
except (TypeError, ValueError):
raise errors.OpPrereqError("Invalid time specification '%s'" % value,
errors.ECODE_INVAL)
return value
def GetOnlineNodes(nodes, cl=None, nowarn=False, secondary_ips=False,
filter_master=False, nodegroup=None):
"""Returns the names of online nodes.
This function will also log a warning on stderr with the names of
the online nodes.
@param nodes: if not empty, use only this subset of nodes (minus the
offline ones)
@param cl: if not None, luxi client to use
@type nowarn: boolean
@param nowarn: by default, this function will output a note with the
offline nodes that are skipped; if this parameter is True the
note is not displayed
@type secondary_ips: boolean
@param secondary_ips: if True, return the secondary IPs instead of the
names, useful for doing network traffic over the replication interface
(if any)
@type filter_master: boolean
@param filter_master: if True, do not return the master node in the list
(useful in coordination with secondary_ips where we cannot check our
node name against the list)
@type nodegroup: string
@param nodegroup: If set, only return nodes in this node group
"""
if cl is None:
cl = GetClient()
qfilter = []
if nodes:
qfilter.append(qlang.MakeSimpleFilter("name", nodes))
if nodegroup is not None:
qfilter.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup],
[qlang.OP_EQUAL, "group.uuid", nodegroup]])
if filter_master:
qfilter.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
if qfilter:
if len(qfilter) > 1:
final_filter = [qlang.OP_AND] + qfilter
else:
assert len(qfilter) == 1
final_filter = qfilter[0]
else:
final_filter = None
result = cl.Query(constants.QR_NODE, ["name", "offline", "sip"], final_filter)
def _IsOffline(row):
(_, (_, offline), _) = row
return offline
def _GetName(row):
((_, name), _, _) = row
return name
def _GetSip(row):
(_, _, (_, sip)) = row
return sip
(offline, online) = compat.partition(result.data, _IsOffline)
if offline and not nowarn:
ToStderr("Note: skipping offline node(s): %s" %
utils.CommaJoin(map(_GetName, offline)))
if secondary_ips:
fn = _GetSip
else:
fn = _GetName
return map(fn, online)
def GetNodesSshPorts(nodes, cl):
"""Retrieves SSH ports of given nodes.
@param nodes: the names of nodes
@type nodes: a list of strings
@param cl: a client to use for the query
@type cl: L{ganeti.luxi.Client}
@return: the list of SSH ports corresponding to the nodes
@rtype: a list of tuples
"""
return [t[0] for t in
cl.QueryNodes(names=nodes,
fields=["ndp/ssh_port"],
use_locking=False)]
def GetNodeUUIDs(nodes, cl):
"""Retrieves the UUIDs of given nodes.
@param nodes: the names of nodes
@type nodes: a list of string
@param cl: a client to use for the query
@type cl: L{ganeti.luxi.Client}
@return: the list of UUIDs corresponding to the nodes
@rtype: a list of tuples
"""
return [t[0] for t in
cl.QueryNodes(names=nodes,
fields=["uuid"],
use_locking=False)]
def _ToStream(stream, txt, *args):
"""Write a message to a stream, bypassing the logging system
@type stream: file object
@param stream: the file to which we should write
@type txt: str
@param txt: the message
"""
try:
if args:
args = tuple(args)
stream.write(txt % args)
else:
stream.write(txt)
stream.write("\n")
stream.flush()
except IOError, err:
if err.errno == errno.EPIPE:
# our terminal went away, we'll exit
sys.exit(constants.EXIT_FAILURE)
else:
raise
def ToStdout(txt, *args):
"""Write a message to stdout only, bypassing the logging system
This is just a wrapper over _ToStream.
@type txt: str
@param txt: the message
"""
_ToStream(sys.stdout, txt, *args)
def ToStdoutAndLoginfo(txt, *args):
"""Write a message to stdout and additionally log it at INFO level"""
ToStdout(txt, *args)
logging.info(txt, *args)
def ToStderr(txt, *args):
"""Write a message to stderr only, bypassing the logging system
This is just a wrapper over _ToStream.
@type txt: str
@param txt: the message
"""
_ToStream(sys.stderr, txt, *args)
class JobExecutor(object):
"""Class which manages the submission and execution of multiple jobs.
Note that instances of this class should not be reused between
GetResults() calls.
"""
def __init__(self, cl=None, verbose=True, opts=None, feedback_fn=None):
self.queue = []
if cl is None:
cl = GetClient()
self.cl = cl
self.verbose = verbose
self.jobs = []
self.opts = opts
self.feedback_fn = feedback_fn
self._counter = itertools.count()
@staticmethod
def _IfName(name, fmt):
"""Helper function for formatting name.
"""
if name:
return fmt % name
return ""
def QueueJob(self, name, *ops):
"""Record a job for later submit.
@type name: string
@param name: a description of the job, will be used in WaitJobSet
"""
SetGenericOpcodeOpts(ops, self.opts)
self.queue.append((self._counter.next(), name, ops))
def AddJobId(self, name, status, job_id):
"""Adds a job ID to the internal queue.
"""
self.jobs.append((self._counter.next(), status, job_id, name))
def SubmitPending(self, each=False):
"""Submit all pending jobs.
"""
if each:
results = []
for (_, _, ops) in self.queue:
# SubmitJob will remove the success status, but raise an exception if
# the submission fails, so we'll notice that anyway.
results.append([True, self.cl.SubmitJob(ops)[0]])
else:
results = self.cl.SubmitManyJobs([ops for (_, _, ops) in self.queue])
for ((status, data), (idx, name, _)) in zip(results, self.queue):
self.jobs.append((idx, status, data, name))
def _ChooseJob(self):
"""Choose a non-waiting/queued job to poll next.
"""
assert self.jobs, "_ChooseJob called with empty job list"
result = self.cl.QueryJobs([i[2] for i in self.jobs[:_CHOOSE_BATCH]],
["status"])
assert result
for job_data, status in zip(self.jobs, result):
if (isinstance(status, list) and status and
status[0] in (constants.JOB_STATUS_QUEUED,
constants.JOB_STATUS_WAITING,
constants.JOB_STATUS_CANCELING)):
# job is still present and waiting
continue
# good candidate found (either running job or lost job)
self.jobs.remove(job_data)
return job_data
# no job found
return self.jobs.pop(0)
def GetResults(self):
"""Wait for and return the results of all jobs.
@rtype: list
@return: list of tuples (success, job results), in the same order
as the submitted jobs; if a job has failed, instead of the result
there will be the error message
"""
if not self.jobs:
self.SubmitPending()
results = []
if self.verbose:
ok_jobs = [row[2] for row in self.jobs if row[1]]
if ok_jobs:
ToStdout("Submitted jobs %s", utils.CommaJoin(ok_jobs))
# first, remove any non-submitted jobs
self.jobs, failures = compat.partition(self.jobs, lambda x: x[1])
for idx, _, jid, name in failures:
ToStderr("Failed to submit job%s: %s", self._IfName(name, " for %s"), jid)
results.append((idx, False, jid))
while self.jobs:
(idx, _, jid, name) = self._ChooseJob()
ToStdout("Waiting for job %s%s ...", jid, self._IfName(name, " for %s"))
try:
job_result = PollJob(jid, cl=self.cl, feedback_fn=self.feedback_fn)
success = True
except errors.JobLost, err:
_, job_result = FormatError(err)
ToStderr("Job %s%s has been archived, cannot check its result",
jid, self._IfName(name, " for %s"))
success = False
except (errors.GenericError, rpcerr.ProtocolError), err:
_, job_result = FormatError(err)
success = False
# the error message will always be shown, verbose or not
ToStderr("Job %s%s has failed: %s",
jid, self._IfName(name, " for %s"), job_result)
results.append((idx, success, job_result))
# sort based on the index, then drop it
results.sort()
results = [i[1:] for i in results]
return results
def WaitOrShow(self, wait):
"""Wait for job results or only print the job IDs.
@type wait: boolean
@param wait: whether to wait or not
"""
if wait:
return self.GetResults()
else:
if not self.jobs:
self.SubmitPending()
for _, status, result, name in self.jobs:
if status:
ToStdout("%s: %s", result, name)
else:
ToStderr("Failure for %s: %s", name, result)
return [row[1:3] for row in self.jobs]
def FormatParamsDictInfo(param_dict, actual, roman=False):
"""Formats a parameter dictionary.
@type param_dict: dict
@param param_dict: the own parameters
@type actual: dict
@param actual: the current parameter set (including defaults)
@rtype: dict
@return: dictionary where the value of each parameter is either a fully
formatted string or a dictionary containing formatted strings
"""
ret = {}
for (key, data) in actual.items():
if isinstance(data, dict) and data:
ret[key] = FormatParamsDictInfo(param_dict.get(key, {}), data, roman)
else:
default_str = "default (%s)" % compat.TryToRoman(data, roman)
ret[key] = str(compat.TryToRoman(param_dict.get(key, default_str), roman))
return ret
def _FormatListInfoDefault(data, def_data):
if data is not None:
ret = utils.CommaJoin(data)
else:
ret = "default (%s)" % utils.CommaJoin(def_data)
return ret
def FormatPolicyInfo(custom_ipolicy, eff_ipolicy, iscluster, roman=False):
"""Formats an instance policy.
@type custom_ipolicy: dict
@param custom_ipolicy: own policy
@type eff_ipolicy: dict
@param eff_ipolicy: effective policy (including defaults); ignored for
cluster
@type iscluster: bool
@param iscluster: the policy is at cluster level
@type roman: bool
@param roman: whether to print the values in roman numerals
@rtype: list of pairs
@return: formatted data, suitable for L{PrintGenericInfo}
"""
if iscluster:
eff_ipolicy = custom_ipolicy
minmax_out = []
custom_minmax = custom_ipolicy.get(constants.ISPECS_MINMAX)
if custom_minmax:
for (k, minmax) in enumerate(custom_minmax):
minmax_out.append([
("%s/%s" % (key, k),
FormatParamsDictInfo(minmax[key], minmax[key], roman))
for key in constants.ISPECS_MINMAX_KEYS
])
else:
for (k, minmax) in enumerate(eff_ipolicy[constants.ISPECS_MINMAX]):
minmax_out.append([
("%s/%s" % (key, k),
FormatParamsDictInfo({}, minmax[key], roman))
for key in constants.ISPECS_MINMAX_KEYS
])
ret = [("bounds specs", minmax_out)]
if iscluster:
stdspecs = custom_ipolicy[constants.ISPECS_STD]
ret.append(
(constants.ISPECS_STD,
FormatParamsDictInfo(stdspecs, stdspecs, roman))
)
ret.append(
("allowed disk templates",
_FormatListInfoDefault(custom_ipolicy.get(constants.IPOLICY_DTS),
eff_ipolicy[constants.IPOLICY_DTS]))
)
to_roman = compat.TryToRoman
ret.extend([
(key, str(to_roman(custom_ipolicy.get(key,
"default (%s)" % eff_ipolicy[key]),
roman)))
for key in constants.IPOLICY_PARAMETERS
])
return ret
def _PrintSpecsParameters(buf, specs):
values = ("%s=%s" % (par, val) for (par, val) in sorted(specs.items()))
buf.write(",".join(values))
def PrintIPolicyCommand(buf, ipolicy, isgroup):
"""Print the command option used to generate the given instance policy.
Currently only the parts dealing with specs are supported.
@type buf: StringIO
@param buf: stream to write into
@type ipolicy: dict
@param ipolicy: instance policy
@type isgroup: bool
@param isgroup: whether the policy is at group level
"""
if not isgroup:
stdspecs = ipolicy.get("std")
if stdspecs:
buf.write(" %s " % IPOLICY_STD_SPECS_STR)
_PrintSpecsParameters(buf, stdspecs)
minmaxes = ipolicy.get("minmax", [])
first = True
for minmax in minmaxes:
minspecs = minmax.get("min")
maxspecs = minmax.get("max")
if minspecs and maxspecs:
if first:
buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR)
first = False
else:
buf.write("//")
buf.write("min:")
_PrintSpecsParameters(buf, minspecs)
buf.write("/max:")
_PrintSpecsParameters(buf, maxspecs)
def ConfirmOperation(names, list_type, text, extra=""):
"""Ask the user to confirm an operation on a list of list_type.
This function is used to request confirmation for doing an operation
on a given list of list_type.
@type names: list
@param names: the list of names that we display when
we ask for confirmation
@type list_type: str
@param list_type: Human readable name for elements in the list (e.g. nodes)
@type text: str
@param text: the operation that the user should confirm
@rtype: boolean
@return: True or False depending on user's confirmation.
"""
count = len(names)
msg = ("The %s will operate on %d %s.\n%s"
"Do you want to continue?" % (text, count, list_type, extra))
affected = (("\nAffected %s:\n" % list_type) +
"\n".join([" %s" % name for name in names]))
choices = [("y", True, "Yes, execute the %s" % text),
("n", False, "No, abort the %s" % text)]
if count > 20:
choices.insert(1, ("v", "v", "View the list of affected %s" % list_type))
question = msg
else:
question = msg + affected
choice = AskUser(question, choices)
if choice == "v":
choices.pop(1)
choice = AskUser(msg + affected, choices)
return choice
def _MaybeParseUnit(elements):
"""Parses and returns an array of potential values with units.
"""
parsed = {}
for k, v in elements.items():
if v == constants.VALUE_DEFAULT:
parsed[k] = v
else:
parsed[k] = utils.ParseUnit(v)
return parsed
def _InitISpecsFromSplitOpts(ipolicy, ispecs_mem_size, ispecs_cpu_count,
ispecs_disk_count, ispecs_disk_size,
ispecs_nic_count, group_ipolicy, fill_all):
try:
if ispecs_mem_size:
ispecs_mem_size = _MaybeParseUnit(ispecs_mem_size)
if ispecs_disk_size:
ispecs_disk_size = _MaybeParseUnit(ispecs_disk_size)
except (TypeError, ValueError, errors.UnitParseError), err:
raise errors.OpPrereqError("Invalid disk (%s) or memory (%s) size"
" in policy: %s" %
(ispecs_disk_size, ispecs_mem_size, err),
errors.ECODE_INVAL)
# prepare ipolicy dict
ispecs_transposed = {
constants.ISPEC_MEM_SIZE: ispecs_mem_size,
constants.ISPEC_CPU_COUNT: ispecs_cpu_count,
constants.ISPEC_DISK_COUNT: ispecs_disk_count,
constants.ISPEC_DISK_SIZE: ispecs_disk_size,
constants.ISPEC_NIC_COUNT: ispecs_nic_count,
}
# first, check that the values given are correct
if group_ipolicy:
forced_type = TISPECS_GROUP_TYPES
else:
forced_type = TISPECS_CLUSTER_TYPES
for specs in ispecs_transposed.values():
assert type(specs) is dict
utils.ForceDictType(specs, forced_type)
# then transpose
ispecs = {
constants.ISPECS_MIN: {},
constants.ISPECS_MAX: {},
constants.ISPECS_STD: {},
}
for (name, specs) in ispecs_transposed.iteritems():
assert name in constants.ISPECS_PARAMETERS
for key, val in specs.items(): # {min: .. ,max: .., std: ..}
assert key in ispecs
ispecs[key][name] = val
minmax_out = {}
for key in constants.ISPECS_MINMAX_KEYS:
if fill_all:
minmax_out[key] = \
objects.FillDict(constants.ISPECS_MINMAX_DEFAULTS[key], ispecs[key])
else:
minmax_out[key] = ispecs[key]
ipolicy[constants.ISPECS_MINMAX] = [minmax_out]
if fill_all:
ipolicy[constants.ISPECS_STD] = \
objects.FillDict(constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
ispecs[constants.ISPECS_STD])
else:
ipolicy[constants.ISPECS_STD] = ispecs[constants.ISPECS_STD]
def _ParseSpecUnit(spec, keyname):
ret = spec.copy()
for k in [constants.ISPEC_DISK_SIZE, constants.ISPEC_MEM_SIZE]:
if k in ret:
try:
ret[k] = utils.ParseUnit(ret[k])
except (TypeError, ValueError, errors.UnitParseError), err:
raise errors.OpPrereqError(("Invalid parameter %s (%s) in %s instance"
" specs: %s" % (k, ret[k], keyname, err)),
errors.ECODE_INVAL)
return ret
def _ParseISpec(spec, keyname, required):
ret = _ParseSpecUnit(spec, keyname)
utils.ForceDictType(ret, constants.ISPECS_PARAMETER_TYPES)
missing = constants.ISPECS_PARAMETERS - frozenset(ret.keys())
if required and missing:
raise errors.OpPrereqError("Missing parameters in ipolicy spec %s: %s" %
(keyname, utils.CommaJoin(missing)),
errors.ECODE_INVAL)
return ret
def _GetISpecsInAllowedValues(minmax_ispecs, allowed_values):
ret = None
if (minmax_ispecs and allowed_values and len(minmax_ispecs) == 1 and
len(minmax_ispecs[0]) == 1):
for (key, spec) in minmax_ispecs[0].items():
# This loop is executed exactly once
if key in allowed_values and not spec:
ret = key
return ret
def _InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs,
group_ipolicy, allowed_values):
found_allowed = _GetISpecsInAllowedValues(minmax_ispecs, allowed_values)
if found_allowed is not None:
ipolicy_out[constants.ISPECS_MINMAX] = found_allowed
elif minmax_ispecs is not None:
minmax_out = []
for mmpair in minmax_ispecs:
mmpair_out = {}
for (key, spec) in mmpair.items():
if key not in constants.ISPECS_MINMAX_KEYS:
msg = "Invalid key in bounds instance specifications: %s" % key
raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
mmpair_out[key] = _ParseISpec(spec, key, True)
minmax_out.append(mmpair_out)
ipolicy_out[constants.ISPECS_MINMAX] = minmax_out
if std_ispecs is not None:
assert not group_ipolicy # This is not an option for gnt-group
ipolicy_out[constants.ISPECS_STD] = _ParseISpec(std_ispecs, "std", False)
def CreateIPolicyFromOpts(ispecs_mem_size=None,
ispecs_cpu_count=None,
ispecs_disk_count=None,
ispecs_disk_size=None,
ispecs_nic_count=None,
minmax_ispecs=None,
std_ispecs=None,
ipolicy_disk_templates=None,
ipolicy_vcpu_ratio=None,
ipolicy_spindle_ratio=None,
group_ipolicy=False,
allowed_values=None,
fill_all=False):
"""Creation of instance policy based on command line options.
@param fill_all: whether for cluster policies we should ensure that
all values are filled
"""
assert not (fill_all and allowed_values)
split_specs = (ispecs_mem_size or ispecs_cpu_count or ispecs_disk_count or
ispecs_disk_size or ispecs_nic_count)
if (split_specs and (minmax_ispecs is not None or std_ispecs is not None)):
raise errors.OpPrereqError("A --specs-xxx option cannot be specified"
" together with any --ipolicy-xxx-specs option",
errors.ECODE_INVAL)
ipolicy_out = objects.MakeEmptyIPolicy()
if split_specs:
assert fill_all
_InitISpecsFromSplitOpts(ipolicy_out, ispecs_mem_size, ispecs_cpu_count,
ispecs_disk_count, ispecs_disk_size,
ispecs_nic_count, group_ipolicy, fill_all)
elif (minmax_ispecs is not None or std_ispecs is not None):
_InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs,
group_ipolicy, allowed_values)
if ipolicy_disk_templates is not None:
if allowed_values and ipolicy_disk_templates in allowed_values:
ipolicy_out[constants.IPOLICY_DTS] = ipolicy_disk_templates
else:
ipolicy_out[constants.IPOLICY_DTS] = list(ipolicy_disk_templates)
if ipolicy_vcpu_ratio is not None:
ipolicy_out[constants.IPOLICY_VCPU_RATIO] = ipolicy_vcpu_ratio
if ipolicy_spindle_ratio is not None:
ipolicy_out[constants.IPOLICY_SPINDLE_RATIO] = ipolicy_spindle_ratio
assert not (frozenset(ipolicy_out.keys()) - constants.IPOLICY_ALL_KEYS)
if not group_ipolicy and fill_all:
ipolicy_out = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy_out)
return ipolicy_out
def _NotAContainer(data):
""" Checks whether the input is not a container data type.
@rtype: bool
"""
return not (isinstance(data, (list, dict, tuple)))
def _GetAlignmentMapping(data):
""" Returns info about alignment if present in an encoded ordered dictionary.
@type data: list of tuple
@param data: The encoded ordered dictionary, as defined in
L{_SerializeGenericInfo}.
@rtype: dict of any to int
@return: The dictionary mapping alignment groups to the maximum length of the
dictionary key found in the group.
"""
alignment_map = {}
for entry in data:
if len(entry) > 2:
group_key = entry[2]
key_length = len(entry[0])
if group_key in alignment_map:
alignment_map[group_key] = max(alignment_map[group_key], key_length)
else:
alignment_map[group_key] = key_length
return alignment_map
def _SerializeGenericInfo(buf, data, level, afterkey=False):
"""Formatting core of L{PrintGenericInfo}.
@param buf: (string) stream to accumulate the result into
@param data: data to format
@type level: int
@param level: depth in the data hierarchy, used for indenting
@type afterkey: bool
@param afterkey: True when we are in the middle of a line after a key (used
to properly add newlines or indentation)
"""
baseind = " "
if isinstance(data, dict):
if not data:
buf.write("\n")
else:
if afterkey:
buf.write("\n")
doindent = True
else:
doindent = False
for key in sorted(data):
if doindent:
buf.write(baseind * level)
else:
doindent = True
buf.write(key)
buf.write(": ")
_SerializeGenericInfo(buf, data[key], level + 1, afterkey=True)
elif isinstance(data, list) and len(data) > 0 and isinstance(data[0], tuple):
# list of tuples (an ordered dictionary)
# the tuples may have two or three members - key, value, and alignment group
# if the alignment group is present, align all values sharing the same group
if afterkey:
buf.write("\n")
doindent = True
else:
doindent = False
alignment_mapping = _GetAlignmentMapping(data)
for entry in data:
key, val = entry[0:2]
if doindent:
buf.write(baseind * level)
else:
doindent = True
buf.write(key)
buf.write(": ")
if len(entry) > 2:
max_key_length = alignment_mapping[entry[2]]
buf.write(" " * (max_key_length - len(key)))
_SerializeGenericInfo(buf, val, level + 1, afterkey=True)
elif isinstance(data, tuple) and all(map(_NotAContainer, data)):
# tuples with simple content are serialized as inline lists
buf.write("[%s]\n" % utils.CommaJoin(data))
elif isinstance(data, list) or isinstance(data, tuple):
# lists and tuples
if not data:
buf.write("\n")
else:
if afterkey:
buf.write("\n")
doindent = True
else:
doindent = False
for item in data:
if doindent:
buf.write(baseind * level)
else:
doindent = True
buf.write("-")
buf.write(baseind[1:])
_SerializeGenericInfo(buf, item, level + 1)
else:
# This branch should be only taken for strings, but it's practically
# impossible to guarantee that no other types are produced somewhere
buf.write(str(data))
buf.write("\n")
def PrintGenericInfo(data):
"""Print information formatted according to the hierarchy.
The output is a valid YAML string.
@param data: the data to print. It's a hierarchical structure whose elements
can be:
- dictionaries, where keys are strings and values are of any of the
types listed here
- lists of tuples (key, value) or (key, value, alignment_group), where
key is a string, value is of any of the types listed here, and
alignment_group can be any hashable value; it's a way to encode
ordered dictionaries; any entries sharing the same alignment group are
aligned by appending whitespace before the value as needed
- lists of any of the types listed here
- strings
"""
buf = StringIO()
_SerializeGenericInfo(buf, data, 0)
ToStdout(buf.getvalue().rstrip("\n"))