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