#
#
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# C0103: Invalid name gnt-node
import itertools
+import errno
from ganeti.cli import *
from ganeti import cli
from ganeti import constants
from ganeti import errors
from ganeti import netutils
-from cStringIO import StringIO
+from ganeti import pathutils
+from ganeti import ssh
+from ganeti import compat
from ganeti import confd
from ganeti.confd import client as confd_client
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
+_OOB_COMMAND_ASK = compat.UniqueFrozenset([
+ constants.OOB_POWER_OFF,
+ constants.OOB_POWER_CYCLE,
+ ])
-_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
- constants.OOB_POWER_CYCLE])
-
-
-_ENV_OVERRIDE = frozenset(["list"])
-
+_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
action="store_false", dest="node_setup",
errors.ECODE_INVAL)
-def _RunSetupSSH(options, nodes):
- """Wrapper around utils.RunCmd to call setup-ssh
+def _TryReadFile(path):
+ """Tries to read a file.
+
+ If the file is not found, C{None} is returned.
+
+ @type path: string
+ @param path: Filename
+ @rtype: None or string
+ @todo: Consider adding a generic ENOENT wrapper
+
+ """
+ try:
+ return utils.ReadFile(path)
+ except EnvironmentError, err:
+ if err.errno == errno.ENOENT:
+ return None
+ else:
+ raise
+
+
+def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
+ """Reads SSH keys according to C{keyfiles}.
- @param options: The command line options
- @param nodes: The nodes to setup
+ @type keyfiles: dict
+ @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
+ tuples (private and public key file)
+ @rtype: list
+ @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
+ public key as strings)
"""
+ result = []
+
+ for (kind, (private_file, public_file)) in keyfiles.items():
+ private_key = _TryReadFile(private_file)
+ public_key = _TryReadFile(public_file)
+
+ if public_key and private_key:
+ result.append((kind, private_key, public_key))
+ elif public_key or private_key:
+ _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
+ " '%s' and '%s'", kind, private_file, public_file)
+
+ return result
- assert nodes, "Empty node list"
- cmd = [constants.SETUP_SSH]
+def _SetupSSH(options, cluster_name, node):
+ """Configures a destination node's SSH daemon.
- # Pass --debug|--verbose to the external script if set on our invocation
- # --debug overrides --verbose
- if options.debug:
- cmd.append("--debug")
- elif options.verbose:
- cmd.append("--verbose")
- if not options.ssh_key_check:
- cmd.append("--no-ssh-key-check")
+ @param options: Command line options
+ @type cluster_name
+ @param cluster_name: Cluster name
+ @type node: string
+ @param node: Destination node name
+
+ """
if options.force_join:
- cmd.append("--force-join")
+ ToStderr("The \"--force-join\" option is no longer supported and will be"
+ " ignored.")
+
+ host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
+
+ (_, root_keyfiles) = \
+ ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
- cmd.extend(nodes)
+ root_keys = _ReadSshKeys(root_keyfiles)
- result = utils.RunCmd(cmd, interactive=True)
+ (_, cert_pem) = \
+ utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
- if result.failed:
- errmsg = ("Command '%s' failed with exit code %s; output %r" %
- (result.cmd, result.exit_code, result.output))
- raise errors.OpExecError(errmsg)
+ data = {
+ constants.SSHS_CLUSTER_NAME: cluster_name,
+ constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
+ constants.SSHS_SSH_HOST_KEY: host_keys,
+ constants.SSHS_SSH_ROOT_KEY: root_keys,
+ }
+
+ bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
+ options.debug, options.verbose, False,
+ options.ssh_key_check, options.ssh_key_check, data)
@UsesRPC
sip = opts.secondary_ip
# read the cluster name from the master
- output = cl.QueryConfigValues(["cluster_name"])
- cluster_name = output[0]
+ (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
if not readd and opts.node_setup:
ToStderr("-- WARNING -- \n"
"and grant full intra-cluster ssh root access to/from it\n", node)
if opts.node_setup:
- _RunSetupSSH(opts, [node])
+ _SetupSSH(opts, cluster_name, node)
- bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
+ bootstrap.SetupNodeDaemon(opts, cluster_name, node)
if opts.disk_state:
disk_state = utils.FlatToDict(opts.disk_state)
fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
(",".join, False))
+ cl = GetClient(query=True)
+
return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
opts.separator, not opts.no_headers,
format_override=fmtoverride, verbose=opts.verbose,
- force_filter=opts.force_filter)
+ force_filter=opts.force_filter, cl=cl)
def ListNodeFields(opts, args):
@return: the desired exit code
"""
+ cl = GetClient(query=True)
+
return GenericListFields(constants.QR_NODE, args, opts.separator,
- not opts.no_headers)
+ not opts.no_headers, cl=cl)
def EvacuateNode(opts, args):
cl = GetClient()
- result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
+ qcl = GetClient(query=True)
+ result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
+ qcl.Close()
+
instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
if not instances:
# these fields are static data anyway, so it doesn't matter, but
# locking=True should be safer
+ qcl = GetClient(query=True)
result = cl.QueryNodes(names=args, fields=selected_fields,
use_locking=False)
+ qcl.Close()
node, pinst = result[0]
if not pinst:
force = opts.force
selected_fields = ["name", "pinst_list"]
+ qcl = GetClient(query=True)
result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
+ qcl.Close()
((node, pinst), ) = result
if not pinst:
return rcode
+def _FormatNodeInfo(node_info):
+ """Format node information for L{cli.PrintGenericInfo()}.
+
+ """
+ (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
+ master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
+ info = [
+ ("Node name", name),
+ ("primary ip", primary_ip),
+ ("secondary ip", secondary_ip),
+ ("master candidate", is_mc),
+ ("drained", drained),
+ ("offline", offline),
+ ]
+ if powered is not None:
+ info.append(("powered", powered))
+ info.extend([
+ ("master_capable", master_capable),
+ ("vm_capable", vm_capable),
+ ])
+ if vm_capable:
+ info.extend([
+ ("primary for instances",
+ [iname for iname in utils.NiceSort(pinst)]),
+ ("secondary for instances",
+ [iname for iname in utils.NiceSort(sinst)]),
+ ])
+ info.append(("node parameters",
+ FormatParamsDictInfo(ndparams_custom, ndparams)))
+ return info
+
+
def ShowNodeConfig(opts, args):
"""Show node information.
@return: the desired exit code
"""
- cl = GetClient()
+ cl = GetClient(query=True)
result = cl.QueryNodes(fields=["name", "pip", "sip",
"pinst_list", "sinst_list",
"master_candidate", "drained", "offline",
"master_capable", "vm_capable", "powered",
"ndparams", "custom_ndparams"],
names=args, use_locking=False)
-
- for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
- master_capable, vm_capable, powered, ndparams,
- ndparams_custom) in result:
- ToStdout("Node name: %s", name)
- ToStdout(" primary ip: %s", primary_ip)
- ToStdout(" secondary ip: %s", secondary_ip)
- ToStdout(" master candidate: %s", is_mc)
- ToStdout(" drained: %s", drained)
- ToStdout(" offline: %s", offline)
- if powered is not None:
- ToStdout(" powered: %s", powered)
- ToStdout(" master_capable: %s", master_capable)
- ToStdout(" vm_capable: %s", vm_capable)
- if vm_capable:
- if pinst:
- ToStdout(" primary for instances:")
- for iname in utils.NiceSort(pinst):
- ToStdout(" - %s", iname)
- else:
- ToStdout(" primary for no instances")
- if sinst:
- ToStdout(" secondary for instances:")
- for iname in utils.NiceSort(sinst):
- ToStdout(" - %s", iname)
- else:
- ToStdout(" secondary for no instances")
- ToStdout(" node parameters:")
- buf = StringIO()
- FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
- ToStdout(buf.getvalue().rstrip("\n"))
-
+ PrintGenericInfo([
+ _FormatNodeInfo(node_info)
+ for node_info in result
+ ])
return 0
return 0
+def RestrictedCommand(opts, args):
+ """Runs a remote command on node(s).
+
+ @param opts: Command line options selected by user
+ @type args: list
+ @param args: Command line arguments
+ @rtype: int
+ @return: Exit code
+
+ """
+ cl = GetClient()
+
+ if len(args) > 1 or opts.nodegroup:
+ # Expand node names
+ nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
+ else:
+ raise errors.OpPrereqError("Node group or node names must be given",
+ errors.ECODE_INVAL)
+
+ op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
+ use_locking=opts.do_locking)
+ result = SubmitOrSend(op, opts, cl=cl)
+
+ exit_code = constants.EXIT_SUCCESS
+
+ for (node, (status, text)) in zip(nodes, result):
+ ToStdout("------------------------------------------------")
+ if status:
+ if opts.show_machine_names:
+ for line in text.splitlines():
+ ToStdout("%s: %s", node, line)
+ else:
+ ToStdout("Node: %s", node)
+ ToStdout(text)
+ else:
+ exit_code = constants.EXIT_FAILURE
+ ToStdout(text)
+
+ return exit_code
+
+
class ReplyStatus(object):
"""Class holding a reply status for synchronous confd clients.
" been enabled at build time.")
return constants.EXIT_FAILURE
- if not constants.HS_CONFD:
- ToStderr("Error: this command requires the Haskell version of confd,"
- " but it has not been enabled at build time.")
- return constants.EXIT_FAILURE
-
status = ReplyStatus()
def ListDrbdConfdCallback(reply):
status.failure = True
node = args[0]
- hmac = utils.ReadFile(constants.CONFD_HMAC_KEY)
+ hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
counting_callback = confd_client.ConfdCountingCallback(filter_callback)
cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
return constants.EXIT_FAILURE
fields = ["node", "minor", "instance", "disk", "role", "peer"]
- headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
- "disk": "Disk", "role": "Role", "peer": "PeerNode"}
+ if opts.no_headers:
+ headers = None
+ else:
+ headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
+ "disk": "Disk", "role": "Role", "peer": "PeerNode"}
data = GenerateTable(separator=opts.separator, headers=headers,
fields=fields, data=sorted(status.answer),
return constants.EXIT_SUCCESS
+
commands = {
"add": (
AddNode, [ArgHost(min=1, max=1)],
"evacuate": (
EvacuateNode, ARGS_ONE_NODE,
[FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
- PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
+ PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT] + SUBMIT_OPTS,
"[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
"Relocate the primary and/or secondary instances from a node"),
"failover": (
MigrateNode, ARGS_ONE_NODE,
[FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
- NORUNTIME_CHGS_OPT, SUBMIT_OPT, PRIORITY_OPT],
+ NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
"[-f] <node>",
"Migrate all the primary instance on a node away from it"
" (only for instances of type drbd)"),
"Lists all available fields for nodes"),
"modify": (
SetNodeParams, ARGS_ONE_NODE,
- [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
+ [FORCE_OPT] + SUBMIT_OPTS +
+ [MC_OPT, DRAINED_OPT, OFFLINE_OPT,
CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
"<node_name>", "Alters the parameters of a node"),
"powercycle": (
PowercycleNode, ARGS_ONE_NODE,
- [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
+ [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
"<node_name>", "Tries to forcefully powercycle a node"),
"power": (
PowerNode,
[ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
ArgNode()],
- [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
- FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
+ SUBMIT_OPTS +
+ [AUTO_PROMOTE_OPT, PRIORITY_OPT,
+ IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT,
+ POWER_DELAY_OPT],
"on|off|cycle|status [nodes...]",
"Change power state of node by calling out-of-band helper."),
"remove": (
[ArgNode(min=1, max=1),
ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
ArgFile(min=1, max=1)],
- [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
+ [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
"<node_name> <storage_type> <name>", "Modify storage volume on a node"),
"repair-storage": (
RepairStorage,
[ArgNode(min=1, max=1),
ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
ArgFile(min=1, max=1)],
- [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
+ [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
"<node_name> <storage_type> <name>",
"Repairs a storage volume on a node"),
"list-tags": (
"<node_name>", "List the tags of the given node"),
"add-tags": (
AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
- [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
+ [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
"<node_name> tag...", "Add tags to the given node"),
"remove-tags": (
RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
- [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
+ [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
"<node_name> tag...", "Remove tags from the given node"),
"health": (
Health, ARGS_MANY_NODES,
ListDrbd, ARGS_ONE_NODE,
[NOHDR_OPT, SEP_OPT],
"[<node_name>]", "Query the list of used DRBD minors on the given node"),
+ "restricted-command": (
+ RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
+ [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT],
+ "<command> <node_name> [<node_name>...]",
+ "Executes a restricted command on node(s)"),
}
#: dictionary with aliases for commands