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