Package ganeti :: Package client :: Module gnt_node
[hide private]
[frames] | no frames]

Source Code for Module ganeti.client.gnt_node

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc. 
   5  # All rights reserved. 
   6  # 
   7  # Redistribution and use in source and binary forms, with or without 
   8  # modification, are permitted provided that the following conditions are 
   9  # met: 
  10  # 
  11  # 1. Redistributions of source code must retain the above copyright notice, 
  12  # this list of conditions and the following disclaimer. 
  13  # 
  14  # 2. Redistributions in binary form must reproduce the above copyright 
  15  # notice, this list of conditions and the following disclaimer in the 
  16  # documentation and/or other materials provided with the distribution. 
  17  # 
  18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
  19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
  20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
  21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
  22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
  25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
  26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
  27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
  28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
  29   
  30  """Node related commands""" 
  31   
  32  # pylint: disable=W0401,W0613,W0614,C0103 
  33  # W0401: Wildcard import ganeti.cli 
  34  # W0613: Unused argument, since all functions follow the same API 
  35  # W0614: Unused import %s from wildcard import (since we need cli) 
  36  # C0103: Invalid name gnt-node 
  37   
  38  import itertools 
  39  import errno 
  40   
  41  from ganeti.cli import * 
  42  from ganeti import cli 
  43  from ganeti import bootstrap 
  44  from ganeti import opcodes 
  45  from ganeti import utils 
  46  from ganeti import constants 
  47  from ganeti import errors 
  48  from ganeti import netutils 
  49  from ganeti import pathutils 
  50  from ganeti import ssh 
  51  from ganeti import compat 
  52   
  53  from ganeti import confd 
  54  from ganeti.confd import client as confd_client 
  55   
  56  #: default list of field for L{ListNodes} 
  57  _LIST_DEF_FIELDS = [ 
  58    "name", "dtotal", "dfree", 
  59    "mtotal", "mnode", "mfree", 
  60    "pinst_cnt", "sinst_cnt", 
  61    ] 
  62   
  63   
  64  #: Default field list for L{ListVolumes} 
  65  _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"] 
  66   
  67   
  68  #: default list of field for L{ListStorage} 
  69  _LIST_STOR_DEF_FIELDS = [ 
  70    constants.SF_NODE, 
  71    constants.SF_TYPE, 
  72    constants.SF_NAME, 
  73    constants.SF_SIZE, 
  74    constants.SF_USED, 
  75    constants.SF_FREE, 
  76    constants.SF_ALLOCATABLE, 
  77    ] 
  78   
  79   
  80  #: default list of power commands 
  81  _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"] 
  82   
  83   
  84  #: headers (and full field list) for L{ListStorage} 
  85  _LIST_STOR_HEADERS = { 
  86    constants.SF_NODE: "Node", 
  87    constants.SF_TYPE: "Type", 
  88    constants.SF_NAME: "Name", 
  89    constants.SF_SIZE: "Size", 
  90    constants.SF_USED: "Used", 
  91    constants.SF_FREE: "Free", 
  92    constants.SF_ALLOCATABLE: "Allocatable", 
  93    } 
  94   
  95   
  96  #: User-facing storage unit types 
  97  _USER_STORAGE_TYPE = { 
  98    constants.ST_FILE: "file", 
  99    constants.ST_LVM_PV: "lvm-pv", 
 100    constants.ST_LVM_VG: "lvm-vg", 
 101    constants.ST_SHARED_FILE: "sharedfile", 
 102    constants.ST_GLUSTER: "gluster", 
 103    } 
 104   
 105  _STORAGE_TYPE_OPT = \ 
 106    cli_option("-t", "--storage-type", 
 107               dest="user_storage_type", 
 108               choices=_USER_STORAGE_TYPE.keys(), 
 109               default=None, 
 110               metavar="STORAGE_TYPE", 
 111               help=("Storage type (%s)" % 
 112                     utils.CommaJoin(_USER_STORAGE_TYPE.keys()))) 
 113   
 114  _REPAIRABLE_STORAGE_TYPES = \ 
 115    [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems() 
 116     if constants.SO_FIX_CONSISTENCY in so] 
 117   
 118  _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys() 
 119   
 120  _OOB_COMMAND_ASK = compat.UniqueFrozenset([ 
 121    constants.OOB_POWER_OFF, 
 122    constants.OOB_POWER_CYCLE, 
 123    ]) 
 124   
 125  _ENV_OVERRIDE = compat.UniqueFrozenset(["list"]) 
 126   
 127  NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True, 
 128                                action="store_false", dest="node_setup", 
 129                                help=("Do not make initial SSH setup on remote" 
 130                                      " node (needs to be done manually)")) 
 131   
 132  IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False, 
 133                                 action="store_true", dest="ignore_status", 
 134                                 help=("Ignore the Node(s) offline status" 
 135                                       " (potentially DANGEROUS)")) 
136 137 138 -def ConvertStorageType(user_storage_type):
139 """Converts a user storage type to its internal name. 140 141 """ 142 try: 143 return _USER_STORAGE_TYPE[user_storage_type] 144 except KeyError: 145 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type, 146 errors.ECODE_INVAL)
147
148 149 -def _TryReadFile(path):
150 """Tries to read a file. 151 152 If the file is not found, C{None} is returned. 153 154 @type path: string 155 @param path: Filename 156 @rtype: None or string 157 @todo: Consider adding a generic ENOENT wrapper 158 159 """ 160 try: 161 return utils.ReadFile(path) 162 except EnvironmentError, err: 163 if err.errno == errno.ENOENT: 164 return None 165 else: 166 raise
167
168 169 -def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
170 """Reads SSH keys according to C{keyfiles}. 171 172 @type keyfiles: dict 173 @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values 174 tuples (private and public key file) 175 @rtype: list 176 @return: List of three-values tuples (L{constants.SSHK_ALL}, private and 177 public key as strings) 178 179 """ 180 result = [] 181 182 for (kind, (private_file, public_file)) in keyfiles.items(): 183 private_key = _TryReadFile(private_file) 184 public_key = _TryReadFile(public_file) 185 186 if public_key and private_key: 187 result.append((kind, private_key, public_key)) 188 elif public_key or private_key: 189 _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files" 190 " '%s' and '%s'", kind, private_file, public_file) 191 192 return result
193
194 195 -def _SetupSSH(options, cluster_name, node, ssh_port):
196 """Configures a destination node's SSH daemon. 197 198 @param options: Command line options 199 @type cluster_name 200 @param cluster_name: Cluster name 201 @type node: string 202 @param node: Destination node name 203 @type ssh_port: int 204 @param ssh_port: Destination node ssh port 205 206 """ 207 if options.force_join: 208 ToStderr("The \"--force-join\" option is no longer supported and will be" 209 " ignored.") 210 211 host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES) 212 213 (_, root_keyfiles) = \ 214 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False) 215 216 root_keys = _ReadSshKeys(root_keyfiles) 217 218 (_, cert_pem) = \ 219 utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE)) 220 221 data = { 222 constants.SSHS_CLUSTER_NAME: cluster_name, 223 constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem, 224 constants.SSHS_SSH_HOST_KEY: host_keys, 225 constants.SSHS_SSH_ROOT_KEY: root_keys, 226 } 227 228 bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN, 229 options.debug, options.verbose, False, 230 options.ssh_key_check, options.ssh_key_check, 231 ssh_port, data)
232
233 234 @UsesRPC 235 -def AddNode(opts, args):
236 """Add a node to the cluster. 237 238 @param opts: the command line options selected by the user 239 @type args: list 240 @param args: should contain only one element, the new node name 241 @rtype: int 242 @return: the desired exit code 243 244 """ 245 cl = GetClient() 246 node = netutils.GetHostname(name=args[0]).name 247 readd = opts.readd 248 249 # Retrieve relevant parameters of the node group. 250 ssh_port = None 251 try: 252 # Passing [] to QueryGroups means query the default group: 253 node_groups = [opts.nodegroup] if opts.nodegroup is not None else [] 254 output = cl.QueryGroups(names=node_groups, fields=["ndp/ssh_port"], 255 use_locking=False) 256 (ssh_port, ) = output[0] 257 except (errors.OpPrereqError, errors.OpExecError): 258 pass 259 260 try: 261 output = cl.QueryNodes(names=[node], 262 fields=["name", "sip", "master", 263 "ndp/ssh_port"], 264 use_locking=False) 265 if len(output) == 0: 266 node_exists = "" 267 sip = None 268 else: 269 node_exists, sip, is_master, ssh_port = output[0] 270 except (errors.OpPrereqError, errors.OpExecError): 271 node_exists = "" 272 sip = None 273 274 if readd: 275 if not node_exists: 276 ToStderr("Node %s not in the cluster" 277 " - please retry without '--readd'", node) 278 return 1 279 if is_master: 280 ToStderr("Node %s is the master, cannot readd", node) 281 return 1 282 else: 283 if node_exists: 284 ToStderr("Node %s already in the cluster (as %s)" 285 " - please retry with '--readd'", node, node_exists) 286 return 1 287 sip = opts.secondary_ip 288 289 # read the cluster name from the master 290 (cluster_name, ) = cl.QueryConfigValues(["cluster_name"]) 291 292 if not readd and opts.node_setup: 293 ToStderr("-- WARNING -- \n" 294 "Performing this operation is going to replace the ssh daemon" 295 " keypair\n" 296 "on the target machine (%s) with the ones of the" 297 " current one\n" 298 "and grant full intra-cluster ssh root access to/from it\n", node) 299 300 if opts.node_setup: 301 _SetupSSH(opts, cluster_name, node, ssh_port) 302 303 bootstrap.SetupNodeDaemon(opts, cluster_name, node, ssh_port) 304 305 if opts.disk_state: 306 disk_state = utils.FlatToDict(opts.disk_state) 307 else: 308 disk_state = {} 309 310 hv_state = dict(opts.hv_state) 311 312 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip, 313 readd=opts.readd, group=opts.nodegroup, 314 vm_capable=opts.vm_capable, ndparams=opts.ndparams, 315 master_capable=opts.master_capable, 316 disk_state=disk_state, 317 hv_state=hv_state) 318 SubmitOpCode(op, opts=opts)
319
320 321 -def ListNodes(opts, args):
322 """List nodes and their properties. 323 324 @param opts: the command line options selected by the user 325 @type args: list 326 @param args: nodes to list, or empty for all 327 @rtype: int 328 @return: the desired exit code 329 330 """ 331 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS) 332 333 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"], 334 (",".join, False)) 335 336 cl = GetClient() 337 338 return GenericList(constants.QR_NODE, selected_fields, args, opts.units, 339 opts.separator, not opts.no_headers, 340 format_override=fmtoverride, verbose=opts.verbose, 341 force_filter=opts.force_filter, cl=cl)
342
343 344 -def ListNodeFields(opts, args):
345 """List node fields. 346 347 @param opts: the command line options selected by the user 348 @type args: list 349 @param args: fields to list, or empty for all 350 @rtype: int 351 @return: the desired exit code 352 353 """ 354 cl = GetClient() 355 356 return GenericListFields(constants.QR_NODE, args, opts.separator, 357 not opts.no_headers, cl=cl)
358
359 360 -def EvacuateNode(opts, args):
361 """Relocate all secondary instance from a node. 362 363 @param opts: the command line options selected by the user 364 @type args: list 365 @param args: should be an empty list 366 @rtype: int 367 @return: the desired exit code 368 369 """ 370 if opts.dst_node is not None: 371 ToStderr("New secondary node given (disabling iallocator), hence evacuating" 372 " secondary instances only.") 373 opts.secondary_only = True 374 opts.primary_only = False 375 376 if opts.secondary_only and opts.primary_only: 377 raise errors.OpPrereqError("Only one of the --primary-only and" 378 " --secondary-only options can be passed", 379 errors.ECODE_INVAL) 380 elif opts.primary_only: 381 mode = constants.NODE_EVAC_PRI 382 elif opts.secondary_only: 383 mode = constants.NODE_EVAC_SEC 384 else: 385 mode = constants.NODE_EVAC_ALL 386 387 # Determine affected instances 388 fields = [] 389 390 if not opts.secondary_only: 391 fields.append("pinst_list") 392 if not opts.primary_only: 393 fields.append("sinst_list") 394 395 cl = GetClient() 396 397 qcl = GetClient() 398 result = qcl.QueryNodes(names=args, fields=fields, use_locking=False) 399 qcl.Close() 400 401 instances = set(itertools.chain(*itertools.chain(*itertools.chain(result)))) 402 403 if not instances: 404 # No instances to evacuate 405 ToStderr("No instances to evacuate on node(s) %s, exiting.", 406 utils.CommaJoin(args)) 407 return constants.EXIT_SUCCESS 408 409 if not (opts.force or 410 AskUser("Relocate instance(s) %s from node(s) %s?" % 411 (utils.CommaJoin(utils.NiceSort(instances)), 412 utils.CommaJoin(args)))): 413 return constants.EXIT_CONFIRMATION 414 415 # Evacuate node 416 op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode, 417 remote_node=opts.dst_node, 418 iallocator=opts.iallocator, 419 early_release=opts.early_release) 420 result = SubmitOrSend(op, opts, cl=cl) 421 422 # Keep track of submitted jobs 423 jex = JobExecutor(cl=cl, opts=opts) 424 425 for (status, job_id) in result[constants.JOB_IDS_KEY]: 426 jex.AddJobId(None, status, job_id) 427 428 results = jex.GetResults() 429 bad_cnt = len([row for row in results if not row[0]]) 430 if bad_cnt == 0: 431 ToStdout("All instances evacuated successfully.") 432 rcode = constants.EXIT_SUCCESS 433 else: 434 ToStdout("There were %s errors during the evacuation.", bad_cnt) 435 rcode = constants.EXIT_FAILURE 436 437 return rcode
438
439 440 -def FailoverNode(opts, args):
441 """Failover all primary instance on a node. 442 443 @param opts: the command line options selected by the user 444 @type args: list 445 @param args: should be an empty list 446 @rtype: int 447 @return: the desired exit code 448 449 """ 450 cl = GetClient() 451 force = opts.force 452 selected_fields = ["name", "pinst_list"] 453 454 # these fields are static data anyway, so it doesn't matter, but 455 # locking=True should be safer 456 qcl = GetClient() 457 result = qcl.QueryNodes(names=args, fields=selected_fields, 458 use_locking=False) 459 qcl.Close() 460 node, pinst = result[0] 461 462 if not pinst: 463 ToStderr("No primary instances on node %s, exiting.", node) 464 return 0 465 466 pinst = utils.NiceSort(pinst) 467 468 retcode = 0 469 470 if not force and not AskUser("Fail over instance(s) %s?" % 471 (",".join("'%s'" % name for name in pinst))): 472 return 2 473 474 jex = JobExecutor(cl=cl, opts=opts) 475 for iname in pinst: 476 op = opcodes.OpInstanceFailover(instance_name=iname, 477 ignore_consistency=opts.ignore_consistency, 478 iallocator=opts.iallocator) 479 jex.QueueJob(iname, op) 480 results = jex.GetResults() 481 bad_cnt = len([row for row in results if not row[0]]) 482 if bad_cnt == 0: 483 ToStdout("All %d instance(s) failed over successfully.", len(results)) 484 else: 485 ToStdout("There were errors during the failover:\n" 486 "%d error(s) out of %d instance(s).", bad_cnt, len(results)) 487 return retcode
488
489 490 -def MigrateNode(opts, args):
491 """Migrate all primary instance on a node. 492 493 """ 494 cl = GetClient() 495 force = opts.force 496 selected_fields = ["name", "pinst_list"] 497 498 qcl = GetClient() 499 result = qcl.QueryNodes(names=args, fields=selected_fields, use_locking=False) 500 qcl.Close() 501 ((node, pinst), ) = result 502 503 if not pinst: 504 ToStdout("No primary instances on node %s, exiting." % node) 505 return 0 506 507 pinst = utils.NiceSort(pinst) 508 509 if not (force or 510 AskUser("Migrate instance(s) %s?" % 511 utils.CommaJoin(utils.NiceSort(pinst)))): 512 return constants.EXIT_CONFIRMATION 513 514 # this should be removed once --non-live is deprecated 515 if not opts.live and opts.migration_mode is not None: 516 raise errors.OpPrereqError("Only one of the --non-live and " 517 "--migration-mode options can be passed", 518 errors.ECODE_INVAL) 519 if not opts.live: # --non-live passed 520 mode = constants.HT_MIGRATION_NONLIVE 521 else: 522 mode = opts.migration_mode 523 524 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode, 525 iallocator=opts.iallocator, 526 target_node=opts.dst_node, 527 allow_runtime_changes=opts.allow_runtime_chgs, 528 ignore_ipolicy=opts.ignore_ipolicy) 529 530 result = SubmitOrSend(op, opts, cl=cl) 531 532 # Keep track of submitted jobs 533 jex = JobExecutor(cl=cl, opts=opts) 534 535 for (status, job_id) in result[constants.JOB_IDS_KEY]: 536 jex.AddJobId(None, status, job_id) 537 538 results = jex.GetResults() 539 bad_cnt = len([row for row in results if not row[0]]) 540 if bad_cnt == 0: 541 ToStdout("All instances migrated successfully.") 542 rcode = constants.EXIT_SUCCESS 543 else: 544 ToStdout("There were %s errors during the node migration.", bad_cnt) 545 rcode = constants.EXIT_FAILURE 546 547 return rcode
548
549 550 -def _FormatNodeInfo(node_info):
551 """Format node information for L{cli.PrintGenericInfo()}. 552 553 """ 554 (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline, 555 master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info 556 info = [ 557 ("Node name", name), 558 ("primary ip", primary_ip), 559 ("secondary ip", secondary_ip), 560 ("master candidate", is_mc), 561 ("drained", drained), 562 ("offline", offline), 563 ] 564 if powered is not None: 565 info.append(("powered", powered)) 566 info.extend([ 567 ("master_capable", master_capable), 568 ("vm_capable", vm_capable), 569 ]) 570 if vm_capable: 571 info.extend([ 572 ("primary for instances", 573 [iname for iname in utils.NiceSort(pinst)]), 574 ("secondary for instances", 575 [iname for iname in utils.NiceSort(sinst)]), 576 ]) 577 info.append(("node parameters", 578 FormatParamsDictInfo(ndparams_custom, ndparams))) 579 return info
580
581 582 -def ShowNodeConfig(opts, args):
583 """Show node information. 584 585 @param opts: the command line options selected by the user 586 @type args: list 587 @param args: should either be an empty list, in which case 588 we show information about all nodes, or should contain 589 a list of nodes to be queried for information 590 @rtype: int 591 @return: the desired exit code 592 593 """ 594 cl = GetClient() 595 result = cl.QueryNodes(fields=["name", "pip", "sip", 596 "pinst_list", "sinst_list", 597 "master_candidate", "drained", "offline", 598 "master_capable", "vm_capable", "powered", 599 "ndparams", "custom_ndparams"], 600 names=args, use_locking=False) 601 PrintGenericInfo([ 602 _FormatNodeInfo(node_info) 603 for node_info in result 604 ]) 605 return 0
606
607 608 -def RemoveNode(opts, args):
609 """Remove a node from the cluster. 610 611 @param opts: the command line options selected by the user 612 @type args: list 613 @param args: should contain only one element, the name of 614 the node to be removed 615 @rtype: int 616 @return: the desired exit code 617 618 """ 619 op = opcodes.OpNodeRemove(node_name=args[0]) 620 SubmitOpCode(op, opts=opts) 621 return 0
622
623 624 -def PowercycleNode(opts, args):
625 """Remove a node from the cluster. 626 627 @param opts: the command line options selected by the user 628 @type args: list 629 @param args: should contain only one element, the name of 630 the node to be removed 631 @rtype: int 632 @return: the desired exit code 633 634 """ 635 node = args[0] 636 if (not opts.confirm and 637 not AskUser("Are you sure you want to hard powercycle node %s?" % node)): 638 return 2 639 640 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force) 641 result = SubmitOrSend(op, opts) 642 if result: 643 ToStderr(result) 644 return 0
645
646 647 -def PowerNode(opts, args):
648 """Change/ask power state of a node. 649 650 @param opts: the command line options selected by the user 651 @type args: list 652 @param args: should contain only one element, the name of 653 the node to be removed 654 @rtype: int 655 @return: the desired exit code 656 657 """ 658 command = args.pop(0) 659 660 if opts.no_headers: 661 headers = None 662 else: 663 headers = {"node": "Node", "status": "Status"} 664 665 if command not in _LIST_POWER_COMMANDS: 666 ToStderr("power subcommand %s not supported." % command) 667 return constants.EXIT_FAILURE 668 669 oob_command = "power-%s" % command 670 671 if oob_command in _OOB_COMMAND_ASK: 672 if not args: 673 ToStderr("Please provide at least one node for this command") 674 return constants.EXIT_FAILURE 675 elif not opts.force and not ConfirmOperation(args, "nodes", 676 "power %s" % command): 677 return constants.EXIT_FAILURE 678 assert len(args) > 0 679 680 opcodelist = [] 681 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF: 682 # TODO: This is a little ugly as we can't catch and revert 683 for node in args: 684 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True, 685 auto_promote=opts.auto_promote)) 686 687 opcodelist.append(opcodes.OpOobCommand(node_names=args, 688 command=oob_command, 689 ignore_status=opts.ignore_status, 690 timeout=opts.oob_timeout, 691 power_delay=opts.power_delay)) 692 693 cli.SetGenericOpcodeOpts(opcodelist, opts) 694 695 job_id = cli.SendJob(opcodelist) 696 697 # We just want the OOB Opcode status 698 # If it fails PollJob gives us the error message in it 699 result = cli.PollJob(job_id)[-1] 700 701 errs = 0 702 data = [] 703 for node_result in result: 704 (node_tuple, data_tuple) = node_result 705 (_, node_name) = node_tuple 706 (data_status, data_node) = data_tuple 707 if data_status == constants.RS_NORMAL: 708 if oob_command == constants.OOB_POWER_STATUS: 709 if data_node[constants.OOB_POWER_STATUS_POWERED]: 710 text = "powered" 711 else: 712 text = "unpowered" 713 data.append([node_name, text]) 714 else: 715 # We don't expect data here, so we just say, it was successfully invoked 716 data.append([node_name, "invoked"]) 717 else: 718 errs += 1 719 data.append([node_name, cli.FormatResultError(data_status, True)]) 720 721 data = GenerateTable(separator=opts.separator, headers=headers, 722 fields=["node", "status"], data=data) 723 724 for line in data: 725 ToStdout(line) 726 727 if errs: 728 return constants.EXIT_FAILURE 729 else: 730 return constants.EXIT_SUCCESS
731
732 733 -def Health(opts, args):
734 """Show health of a node using OOB. 735 736 @param opts: the command line options selected by the user 737 @type args: list 738 @param args: should contain only one element, the name of 739 the node to be removed 740 @rtype: int 741 @return: the desired exit code 742 743 """ 744 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH, 745 timeout=opts.oob_timeout) 746 result = SubmitOpCode(op, opts=opts) 747 748 if opts.no_headers: 749 headers = None 750 else: 751 headers = {"node": "Node", "status": "Status"} 752 753 errs = 0 754 data = [] 755 for node_result in result: 756 (node_tuple, data_tuple) = node_result 757 (_, node_name) = node_tuple 758 (data_status, data_node) = data_tuple 759 if data_status == constants.RS_NORMAL: 760 data.append([node_name, "%s=%s" % tuple(data_node[0])]) 761 for item, status in data_node[1:]: 762 data.append(["", "%s=%s" % (item, status)]) 763 else: 764 errs += 1 765 data.append([node_name, cli.FormatResultError(data_status, True)]) 766 767 data = GenerateTable(separator=opts.separator, headers=headers, 768 fields=["node", "status"], data=data) 769 770 for line in data: 771 ToStdout(line) 772 773 if errs: 774 return constants.EXIT_FAILURE 775 else: 776 return constants.EXIT_SUCCESS
777
778 779 -def ListVolumes(opts, args):
780 """List logical volumes on node(s). 781 782 @param opts: the command line options selected by the user 783 @type args: list 784 @param args: should either be an empty list, in which case 785 we list data for all nodes, or contain a list of nodes 786 to display data only for those 787 @rtype: int 788 @return: the desired exit code 789 790 """ 791 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS) 792 793 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields) 794 output = SubmitOpCode(op, opts=opts) 795 796 if not opts.no_headers: 797 headers = {"node": "Node", "phys": "PhysDev", 798 "vg": "VG", "name": "Name", 799 "size": "Size", "instance": "Instance"} 800 else: 801 headers = None 802 803 unitfields = ["size"] 804 805 numfields = ["size"] 806 807 data = GenerateTable(separator=opts.separator, headers=headers, 808 fields=selected_fields, unitfields=unitfields, 809 numfields=numfields, data=output, units=opts.units) 810 811 for line in data: 812 ToStdout(line) 813 814 return 0
815
816 817 -def ListStorage(opts, args):
818 """List physical volumes on node(s). 819 820 @param opts: the command line options selected by the user 821 @type args: list 822 @param args: should either be an empty list, in which case 823 we list data for all nodes, or contain a list of nodes 824 to display data only for those 825 @rtype: int 826 @return: the desired exit code 827 828 """ 829 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS) 830 831 op = opcodes.OpNodeQueryStorage(nodes=args, 832 storage_type=opts.user_storage_type, 833 output_fields=selected_fields) 834 output = SubmitOpCode(op, opts=opts) 835 836 if not opts.no_headers: 837 headers = { 838 constants.SF_NODE: "Node", 839 constants.SF_TYPE: "Type", 840 constants.SF_NAME: "Name", 841 constants.SF_SIZE: "Size", 842 constants.SF_USED: "Used", 843 constants.SF_FREE: "Free", 844 constants.SF_ALLOCATABLE: "Allocatable", 845 } 846 else: 847 headers = None 848 849 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE] 850 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE] 851 852 # change raw values to nicer strings 853 for row in output: 854 for idx, field in enumerate(selected_fields): 855 val = row[idx] 856 if field == constants.SF_ALLOCATABLE: 857 if val: 858 val = "Y" 859 else: 860 val = "N" 861 row[idx] = str(val) 862 863 data = GenerateTable(separator=opts.separator, headers=headers, 864 fields=selected_fields, unitfields=unitfields, 865 numfields=numfields, data=output, units=opts.units) 866 867 for line in data: 868 ToStdout(line) 869 870 return 0
871
872 873 -def ModifyStorage(opts, args):
874 """Modify storage volume on a node. 875 876 @param opts: the command line options selected by the user 877 @type args: list 878 @param args: should contain 3 items: node name, storage type and volume name 879 @rtype: int 880 @return: the desired exit code 881 882 """ 883 (node_name, user_storage_type, volume_name) = args 884 885 storage_type = ConvertStorageType(user_storage_type) 886 887 changes = {} 888 889 if opts.allocatable is not None: 890 changes[constants.SF_ALLOCATABLE] = opts.allocatable 891 892 if changes: 893 op = opcodes.OpNodeModifyStorage(node_name=node_name, 894 storage_type=storage_type, 895 name=volume_name, 896 changes=changes) 897 SubmitOrSend(op, opts) 898 else: 899 ToStderr("No changes to perform, exiting.")
900
901 902 -def RepairStorage(opts, args):
903 """Repairs a storage volume on a node. 904 905 @param opts: the command line options selected by the user 906 @type args: list 907 @param args: should contain 3 items: node name, storage type and volume name 908 @rtype: int 909 @return: the desired exit code 910 911 """ 912 (node_name, user_storage_type, volume_name) = args 913 914 storage_type = ConvertStorageType(user_storage_type) 915 916 op = opcodes.OpRepairNodeStorage(node_name=node_name, 917 storage_type=storage_type, 918 name=volume_name, 919 ignore_consistency=opts.ignore_consistency) 920 SubmitOrSend(op, opts)
921
922 923 -def SetNodeParams(opts, args):
924 """Modifies a node. 925 926 @param opts: the command line options selected by the user 927 @type args: list 928 @param args: should contain only one element, the node name 929 @rtype: int 930 @return: the desired exit code 931 932 """ 933 all_changes = [opts.master_candidate, opts.drained, opts.offline, 934 opts.master_capable, opts.vm_capable, opts.secondary_ip, 935 opts.ndparams] 936 if (all_changes.count(None) == len(all_changes) and 937 not (opts.hv_state or opts.disk_state)): 938 ToStderr("Please give at least one of the parameters.") 939 return 1 940 941 if opts.disk_state: 942 disk_state = utils.FlatToDict(opts.disk_state) 943 else: 944 disk_state = {} 945 946 hv_state = dict(opts.hv_state) 947 948 op = opcodes.OpNodeSetParams(node_name=args[0], 949 master_candidate=opts.master_candidate, 950 offline=opts.offline, 951 drained=opts.drained, 952 master_capable=opts.master_capable, 953 vm_capable=opts.vm_capable, 954 secondary_ip=opts.secondary_ip, 955 force=opts.force, 956 ndparams=opts.ndparams, 957 auto_promote=opts.auto_promote, 958 powered=opts.node_powered, 959 hv_state=hv_state, 960 disk_state=disk_state) 961 962 # even if here we process the result, we allow submit only 963 result = SubmitOrSend(op, opts) 964 965 if result: 966 ToStdout("Modified node %s", args[0]) 967 for param, data in result: 968 ToStdout(" - %-5s -> %s", param, data) 969 return 0
970
971 972 -def RestrictedCommand(opts, args):
973 """Runs a remote command on node(s). 974 975 @param opts: Command line options selected by user 976 @type args: list 977 @param args: Command line arguments 978 @rtype: int 979 @return: Exit code 980 981 """ 982 cl = GetClient() 983 984 if len(args) > 1 or opts.nodegroup: 985 # Expand node names 986 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup) 987 else: 988 raise errors.OpPrereqError("Node group or node names must be given", 989 errors.ECODE_INVAL) 990 991 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes, 992 use_locking=opts.do_locking) 993 result = SubmitOrSend(op, opts, cl=cl) 994 995 exit_code = constants.EXIT_SUCCESS 996 997 for (node, (status, text)) in zip(nodes, result): 998 ToStdout("------------------------------------------------") 999 if status: 1000 if opts.show_machine_names: 1001 for line in text.splitlines(): 1002 ToStdout("%s: %s", node, line) 1003 else: 1004 ToStdout("Node: %s", node) 1005 ToStdout(text) 1006 else: 1007 exit_code = constants.EXIT_FAILURE 1008 ToStdout(text) 1009 1010 return exit_code
1011
1012 1013 -class ReplyStatus(object):
1014 """Class holding a reply status for synchronous confd clients. 1015 1016 """
1017 - def __init__(self):
1018 self.failure = True 1019 self.answer = False
1020
1021 1022 -def ListDrbd(opts, args):
1023 """Modifies a node. 1024 1025 @param opts: the command line options selected by the user 1026 @type args: list 1027 @param args: should contain only one element, the node name 1028 @rtype: int 1029 @return: the desired exit code 1030 1031 """ 1032 if len(args) != 1: 1033 ToStderr("Please give one (and only one) node.") 1034 return constants.EXIT_FAILURE 1035 1036 if not constants.ENABLE_CONFD: 1037 ToStderr("Error: this command requires confd support, but it has not" 1038 " been enabled at build time.") 1039 return constants.EXIT_FAILURE 1040 1041 status = ReplyStatus() 1042 1043 def ListDrbdConfdCallback(reply): 1044 """Callback for confd queries""" 1045 if reply.type == confd_client.UPCALL_REPLY: 1046 answer = reply.server_reply.answer 1047 reqtype = reply.orig_request.type 1048 if reqtype == constants.CONFD_REQ_NODE_DRBD: 1049 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK: 1050 ToStderr("Query gave non-ok status '%s': %s" % 1051 (reply.server_reply.status, 1052 reply.server_reply.answer)) 1053 status.failure = True 1054 return 1055 if not confd.HTNodeDrbd(answer): 1056 ToStderr("Invalid response from server: expected %s, got %s", 1057 confd.HTNodeDrbd, answer) 1058 status.failure = True 1059 else: 1060 status.failure = False 1061 status.answer = answer 1062 else: 1063 ToStderr("Unexpected reply %s!?", reqtype) 1064 status.failure = True
1065 1066 node = args[0] 1067 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY) 1068 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback) 1069 counting_callback = confd_client.ConfdCountingCallback(filter_callback) 1070 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST], 1071 counting_callback) 1072 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD, 1073 query=node) 1074 1075 def DoConfdRequestReply(req): 1076 counting_callback.RegisterQuery(req.rsalt) 1077 cf_client.SendRequest(req, async=False) 1078 while not counting_callback.AllAnswered(): 1079 if not cf_client.ReceiveReply(): 1080 ToStderr("Did not receive all expected confd replies") 1081 break 1082 1083 DoConfdRequestReply(req) 1084 1085 if status.failure: 1086 return constants.EXIT_FAILURE 1087 1088 fields = ["node", "minor", "instance", "disk", "role", "peer"] 1089 if opts.no_headers: 1090 headers = None 1091 else: 1092 headers = {"node": "Node", "minor": "Minor", "instance": "Instance", 1093 "disk": "Disk", "role": "Role", "peer": "PeerNode"} 1094 1095 data = GenerateTable(separator=opts.separator, headers=headers, 1096 fields=fields, data=sorted(status.answer), 1097 numfields=["minor"]) 1098 for line in data: 1099 ToStdout(line) 1100 1101 return constants.EXIT_SUCCESS 1102 1103 1104 commands = { 1105 "add": ( 1106 AddNode, [ArgHost(min=1, max=1)], 1107 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT, 1108 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, 1109 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT, 1110 DISK_STATE_OPT], 1111 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]" 1112 " [--no-node-setup] [--verbose] [--network] <node_name>", 1113 "Add a node to the cluster"), 1114 "evacuate": ( 1115 EvacuateNode, ARGS_ONE_NODE, 1116 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT, 1117 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT] + SUBMIT_OPTS, 1118 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>", 1119 "Relocate the primary and/or secondary instances from a node"), 1120 "failover": ( 1121 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, 1122 IALLOCATOR_OPT, PRIORITY_OPT], 1123 "[-f] <node>", 1124 "Stops the primary instances on a node and start them on their" 1125 " secondary node (only for instances with drbd disk template)"), 1126 "migrate": ( 1127 MigrateNode, ARGS_ONE_NODE, 1128 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT, 1129 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT, 1130 NORUNTIME_CHGS_OPT] + SUBMIT_OPTS, 1131 "[-f] <node>", 1132 "Migrate all the primary instance on a node away from it" 1133 " (only for instances of type drbd)"), 1134 "info": ( 1135 ShowNodeConfig, ARGS_MANY_NODES, [], 1136 "[<node_name>...]", "Show information about the node(s)"), 1137 "list": ( 1138 ListNodes, ARGS_MANY_NODES, 1139 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT, 1140 FORCE_FILTER_OPT], 1141 "[nodes...]", 1142 "Lists the nodes in the cluster. The available fields can be shown using" 1143 " the \"list-fields\" command (see the man page for details)." 1144 " The default field list is (in order): %s." % 1145 utils.CommaJoin(_LIST_DEF_FIELDS)), 1146 "list-fields": ( 1147 ListNodeFields, [ArgUnknown()], 1148 [NOHDR_OPT, SEP_OPT], 1149 "[fields...]", 1150 "Lists all available fields for nodes"), 1151 "modify": ( 1152 SetNodeParams, ARGS_ONE_NODE, 1153 [FORCE_OPT] + SUBMIT_OPTS + 1154 [MC_OPT, DRAINED_OPT, OFFLINE_OPT, 1155 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT, 1156 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT, 1157 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT], 1158 "<node_name>", "Alters the parameters of a node"), 1159 "powercycle": ( 1160 PowercycleNode, ARGS_ONE_NODE, 1161 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1162 "<node_name>", "Tries to forcefully powercycle a node"), 1163 "power": ( 1164 PowerNode, 1165 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS), 1166 ArgNode()], 1167 SUBMIT_OPTS + 1168 [AUTO_PROMOTE_OPT, PRIORITY_OPT, 1169 IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, 1170 POWER_DELAY_OPT], 1171 "on|off|cycle|status [nodes...]", 1172 "Change power state of node by calling out-of-band helper."), 1173 "remove": ( 1174 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT], 1175 "<node_name>", "Removes a node from the cluster"), 1176 "volumes": ( 1177 ListVolumes, [ArgNode()], 1178 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT], 1179 "[<node_name>...]", "List logical volumes on node(s)"), 1180 "list-storage": ( 1181 ListStorage, ARGS_MANY_NODES, 1182 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT, 1183 PRIORITY_OPT], 1184 "[<node_name>...]", "List physical volumes on node(s). The available" 1185 " fields are (see the man page for details): %s." % 1186 (utils.CommaJoin(_LIST_STOR_HEADERS))), 1187 "modify-storage": ( 1188 ModifyStorage, 1189 [ArgNode(min=1, max=1), 1190 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES), 1191 ArgFile(min=1, max=1)], 1192 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1193 "<node_name> <storage_type> <name>", "Modify storage volume on a node"), 1194 "repair-storage": ( 1195 RepairStorage, 1196 [ArgNode(min=1, max=1), 1197 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES), 1198 ArgFile(min=1, max=1)], 1199 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1200 "<node_name> <storage_type> <name>", 1201 "Repairs a storage volume on a node"), 1202 "list-tags": ( 1203 ListTags, ARGS_ONE_NODE, [], 1204 "<node_name>", "List the tags of the given node"), 1205 "add-tags": ( 1206 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], 1207 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1208 "<node_name> tag...", "Add tags to the given node"), 1209 "remove-tags": ( 1210 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], 1211 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1212 "<node_name> tag...", "Remove tags from the given node"), 1213 "health": ( 1214 Health, ARGS_MANY_NODES, 1215 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT], 1216 "[<node_name>...]", "List health of node(s) using out-of-band"), 1217 "list-drbd": ( 1218 ListDrbd, ARGS_ONE_NODE, 1219 [NOHDR_OPT, SEP_OPT], 1220 "[<node_name>]", "Query the list of used DRBD minors on the given node"), 1221 "restricted-command": ( 1222 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES, 1223 [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT], 1224 "<command> <node_name> [<node_name>...]", 1225 "Executes a restricted command on node(s)"), 1226 } 1227 1228 #: dictionary with aliases for commands 1229 aliases = { 1230 "show": "info", 1231 }
1232 1233 1234 -def Main():
1235 return GenericMain(commands, aliases=aliases, 1236 override={"tag_type": constants.TAG_NODE}, 1237 env_override=_ENV_OVERRIDE)
1238