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