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

Source Code for Module ganeti.client.gnt_node

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