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 result = SubmitOrSend(op, opts, cl=cl) 449 450 # Keep track of submitted jobs 451 jex = JobExecutor(cl=cl, opts=opts) 452 453 for (status, job_id) in result[constants.JOB_IDS_KEY]: 454 jex.AddJobId(None, status, job_id) 455 456 results = jex.GetResults() 457 bad_cnt = len([row for row in results if not row[0]]) 458 if bad_cnt == 0: 459 ToStdout("All instances evacuated successfully.") 460 rcode = constants.EXIT_SUCCESS 461 else: 462 ToStdout("There were %s errors during the evacuation.", bad_cnt) 463 rcode = constants.EXIT_FAILURE 464 465 return rcode
466
467 468 -def FailoverNode(opts, args):
469 """Failover all primary instance on a node. 470 471 @param opts: the command line options selected by the user 472 @type args: list 473 @param args: should be an empty list 474 @rtype: int 475 @return: the desired exit code 476 477 """ 478 cl = GetClient() 479 force = opts.force 480 selected_fields = ["name", "pinst_list"] 481 482 # these fields are static data anyway, so it doesn't matter, but 483 # locking=True should be safer 484 qcl = GetClient() 485 result = qcl.QueryNodes(names=args, fields=selected_fields, 486 use_locking=False) 487 qcl.Close() 488 node, pinst = result[0] 489 490 if not pinst: 491 ToStderr("No primary instances on node %s, exiting.", node) 492 return 0 493 494 pinst = utils.NiceSort(pinst) 495 496 retcode = 0 497 498 if not force and not AskUser("Fail over instance(s) %s?" % 499 (",".join("'%s'" % name for name in pinst))): 500 return 2 501 502 jex = JobExecutor(cl=cl, opts=opts) 503 for iname in pinst: 504 op = opcodes.OpInstanceFailover(instance_name=iname, 505 ignore_consistency=opts.ignore_consistency, 506 iallocator=opts.iallocator) 507 jex.QueueJob(iname, op) 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 %d instance(s) failed over successfully.", len(results)) 512 else: 513 ToStdout("There were errors during the failover:\n" 514 "%d error(s) out of %d instance(s).", bad_cnt, len(results)) 515 return retcode
516
517 518 -def MigrateNode(opts, args):
519 """Migrate all primary instance on a node. 520 521 """ 522 cl = GetClient() 523 force = opts.force 524 selected_fields = ["name", "pinst_list"] 525 526 qcl = GetClient() 527 result = qcl.QueryNodes(names=args, fields=selected_fields, use_locking=False) 528 qcl.Close() 529 ((node, pinst), ) = result 530 531 if not pinst: 532 ToStdout("No primary instances on node %s, exiting." % node) 533 return 0 534 535 pinst = utils.NiceSort(pinst) 536 537 if not (force or 538 AskUser("Migrate instance(s) %s?" % 539 utils.CommaJoin(utils.NiceSort(pinst)))): 540 return constants.EXIT_CONFIRMATION 541 542 # this should be removed once --non-live is deprecated 543 if not opts.live and opts.migration_mode is not None: 544 raise errors.OpPrereqError("Only one of the --non-live and " 545 "--migration-mode options can be passed", 546 errors.ECODE_INVAL) 547 if not opts.live: # --non-live passed 548 mode = constants.HT_MIGRATION_NONLIVE 549 else: 550 mode = opts.migration_mode 551 552 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode, 553 iallocator=opts.iallocator, 554 target_node=opts.dst_node, 555 allow_runtime_changes=opts.allow_runtime_chgs, 556 ignore_ipolicy=opts.ignore_ipolicy) 557 558 result = SubmitOrSend(op, opts, cl=cl) 559 560 # Keep track of submitted jobs 561 jex = JobExecutor(cl=cl, opts=opts) 562 563 for (status, job_id) in result[constants.JOB_IDS_KEY]: 564 jex.AddJobId(None, status, job_id) 565 566 results = jex.GetResults() 567 bad_cnt = len([row for row in results if not row[0]]) 568 if bad_cnt == 0: 569 ToStdout("All instances migrated successfully.") 570 rcode = constants.EXIT_SUCCESS 571 else: 572 ToStdout("There were %s errors during the node migration.", bad_cnt) 573 rcode = constants.EXIT_FAILURE 574 575 return rcode
576
577 578 -def _FormatNodeInfo(node_info):
579 """Format node information for L{cli.PrintGenericInfo()}. 580 581 """ 582 (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline, 583 master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info 584 info = [ 585 ("Node name", name), 586 ("primary ip", primary_ip), 587 ("secondary ip", secondary_ip), 588 ("master candidate", is_mc), 589 ("drained", drained), 590 ("offline", offline), 591 ] 592 if powered is not None: 593 info.append(("powered", powered)) 594 info.extend([ 595 ("master_capable", master_capable), 596 ("vm_capable", vm_capable), 597 ]) 598 if vm_capable: 599 info.extend([ 600 ("primary for instances", 601 [iname for iname in utils.NiceSort(pinst)]), 602 ("secondary for instances", 603 [iname for iname in utils.NiceSort(sinst)]), 604 ]) 605 info.append(("node parameters", 606 FormatParamsDictInfo(ndparams_custom, ndparams))) 607 return info
608
609 610 -def ShowNodeConfig(opts, args):
611 """Show node information. 612 613 @param opts: the command line options selected by the user 614 @type args: list 615 @param args: should either be an empty list, in which case 616 we show information about all nodes, or should contain 617 a list of nodes to be queried for information 618 @rtype: int 619 @return: the desired exit code 620 621 """ 622 cl = GetClient() 623 result = cl.QueryNodes(fields=["name", "pip", "sip", 624 "pinst_list", "sinst_list", 625 "master_candidate", "drained", "offline", 626 "master_capable", "vm_capable", "powered", 627 "ndparams", "custom_ndparams"], 628 names=args, use_locking=False) 629 PrintGenericInfo([ 630 _FormatNodeInfo(node_info) 631 for node_info in result 632 ]) 633 return 0
634
635 636 -def RemoveNode(opts, args):
637 """Remove a node from the cluster. 638 639 @param opts: the command line options selected by the user 640 @type args: list 641 @param args: should contain only one element, the name of 642 the node to be removed 643 @rtype: int 644 @return: the desired exit code 645 646 """ 647 op = opcodes.OpNodeRemove(node_name=args[0]) 648 SubmitOpCode(op, opts=opts) 649 return 0
650
651 652 -def PowercycleNode(opts, args):
653 """Remove a node from the cluster. 654 655 @param opts: the command line options selected by the user 656 @type args: list 657 @param args: should contain only one element, the name of 658 the node to be removed 659 @rtype: int 660 @return: the desired exit code 661 662 """ 663 node = args[0] 664 if (not opts.confirm and 665 not AskUser("Are you sure you want to hard powercycle node %s?" % node)): 666 return 2 667 668 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force) 669 result = SubmitOrSend(op, opts) 670 if result: 671 ToStderr(result) 672 return 0
673
674 675 -def PowerNode(opts, args):
676 """Change/ask power state of a node. 677 678 @param opts: the command line options selected by the user 679 @type args: list 680 @param args: should contain only one element, the name of 681 the node to be removed 682 @rtype: int 683 @return: the desired exit code 684 685 """ 686 command = args.pop(0) 687 688 if opts.no_headers: 689 headers = None 690 else: 691 headers = {"node": "Node", "status": "Status"} 692 693 if command not in _LIST_POWER_COMMANDS: 694 ToStderr("power subcommand %s not supported." % command) 695 return constants.EXIT_FAILURE 696 697 oob_command = "power-%s" % command 698 699 if oob_command in _OOB_COMMAND_ASK: 700 if not args: 701 ToStderr("Please provide at least one node for this command") 702 return constants.EXIT_FAILURE 703 elif not opts.force and not ConfirmOperation(args, "nodes", 704 "power %s" % command): 705 return constants.EXIT_FAILURE 706 assert len(args) > 0 707 708 opcodelist = [] 709 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF: 710 # TODO: This is a little ugly as we can't catch and revert 711 for node in args: 712 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True, 713 auto_promote=opts.auto_promote)) 714 715 opcodelist.append(opcodes.OpOobCommand(node_names=args, 716 command=oob_command, 717 ignore_status=opts.ignore_status, 718 timeout=opts.oob_timeout, 719 power_delay=opts.power_delay)) 720 721 cli.SetGenericOpcodeOpts(opcodelist, opts) 722 723 job_id = cli.SendJob(opcodelist) 724 725 # We just want the OOB Opcode status 726 # If it fails PollJob gives us the error message in it 727 result = cli.PollJob(job_id)[-1] 728 729 errs = 0 730 data = [] 731 for node_result in result: 732 (node_tuple, data_tuple) = node_result 733 (_, node_name) = node_tuple 734 (data_status, data_node) = data_tuple 735 if data_status == constants.RS_NORMAL: 736 if oob_command == constants.OOB_POWER_STATUS: 737 if data_node[constants.OOB_POWER_STATUS_POWERED]: 738 text = "powered" 739 else: 740 text = "unpowered" 741 data.append([node_name, text]) 742 else: 743 # We don't expect data here, so we just say, it was successfully invoked 744 data.append([node_name, "invoked"]) 745 else: 746 errs += 1 747 data.append([node_name, cli.FormatResultError(data_status, True)]) 748 749 data = GenerateTable(separator=opts.separator, headers=headers, 750 fields=["node", "status"], data=data) 751 752 for line in data: 753 ToStdout(line) 754 755 if errs: 756 return constants.EXIT_FAILURE 757 else: 758 return constants.EXIT_SUCCESS
759
760 761 -def Health(opts, args):
762 """Show health of a node using OOB. 763 764 @param opts: the command line options selected by the user 765 @type args: list 766 @param args: should contain only one element, the name of 767 the node to be removed 768 @rtype: int 769 @return: the desired exit code 770 771 """ 772 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH, 773 timeout=opts.oob_timeout) 774 result = SubmitOpCode(op, opts=opts) 775 776 if opts.no_headers: 777 headers = None 778 else: 779 headers = {"node": "Node", "status": "Status"} 780 781 errs = 0 782 data = [] 783 for node_result in result: 784 (node_tuple, data_tuple) = node_result 785 (_, node_name) = node_tuple 786 (data_status, data_node) = data_tuple 787 if data_status == constants.RS_NORMAL: 788 data.append([node_name, "%s=%s" % tuple(data_node[0])]) 789 for item, status in data_node[1:]: 790 data.append(["", "%s=%s" % (item, status)]) 791 else: 792 errs += 1 793 data.append([node_name, cli.FormatResultError(data_status, True)]) 794 795 data = GenerateTable(separator=opts.separator, headers=headers, 796 fields=["node", "status"], data=data) 797 798 for line in data: 799 ToStdout(line) 800 801 if errs: 802 return constants.EXIT_FAILURE 803 else: 804 return constants.EXIT_SUCCESS
805
806 807 -def ListVolumes(opts, args):
808 """List logical volumes on node(s). 809 810 @param opts: the command line options selected by the user 811 @type args: list 812 @param args: should either be an empty list, in which case 813 we list data for all nodes, or contain a list of nodes 814 to display data only for those 815 @rtype: int 816 @return: the desired exit code 817 818 """ 819 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS) 820 821 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields) 822 output = SubmitOpCode(op, opts=opts) 823 824 if not opts.no_headers: 825 headers = {"node": "Node", "phys": "PhysDev", 826 "vg": "VG", "name": "Name", 827 "size": "Size", "instance": "Instance"} 828 else: 829 headers = None 830 831 unitfields = ["size"] 832 833 numfields = ["size"] 834 835 data = GenerateTable(separator=opts.separator, headers=headers, 836 fields=selected_fields, unitfields=unitfields, 837 numfields=numfields, data=output, units=opts.units) 838 839 for line in data: 840 ToStdout(line) 841 842 return 0
843
844 845 -def ListStorage(opts, args):
846 """List physical volumes on node(s). 847 848 @param opts: the command line options selected by the user 849 @type args: list 850 @param args: should either be an empty list, in which case 851 we list data for all nodes, or contain a list of nodes 852 to display data only for those 853 @rtype: int 854 @return: the desired exit code 855 856 """ 857 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS) 858 859 op = opcodes.OpNodeQueryStorage(nodes=args, 860 storage_type=opts.user_storage_type, 861 output_fields=selected_fields) 862 output = SubmitOpCode(op, opts=opts) 863 864 if not opts.no_headers: 865 headers = { 866 constants.SF_NODE: "Node", 867 constants.SF_TYPE: "Type", 868 constants.SF_NAME: "Name", 869 constants.SF_SIZE: "Size", 870 constants.SF_USED: "Used", 871 constants.SF_FREE: "Free", 872 constants.SF_ALLOCATABLE: "Allocatable", 873 } 874 else: 875 headers = None 876 877 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE] 878 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE] 879 880 # change raw values to nicer strings 881 for row in output: 882 for idx, field in enumerate(selected_fields): 883 val = row[idx] 884 if field == constants.SF_ALLOCATABLE: 885 if val: 886 val = "Y" 887 else: 888 val = "N" 889 row[idx] = str(val) 890 891 data = GenerateTable(separator=opts.separator, headers=headers, 892 fields=selected_fields, unitfields=unitfields, 893 numfields=numfields, data=output, units=opts.units) 894 895 for line in data: 896 ToStdout(line) 897 898 return 0
899
900 901 -def ModifyStorage(opts, args):
902 """Modify storage volume on a node. 903 904 @param opts: the command line options selected by the user 905 @type args: list 906 @param args: should contain 3 items: node name, storage type and volume name 907 @rtype: int 908 @return: the desired exit code 909 910 """ 911 (node_name, user_storage_type, volume_name) = args 912 913 storage_type = ConvertStorageType(user_storage_type) 914 915 changes = {} 916 917 if opts.allocatable is not None: 918 changes[constants.SF_ALLOCATABLE] = opts.allocatable 919 920 if changes: 921 op = opcodes.OpNodeModifyStorage(node_name=node_name, 922 storage_type=storage_type, 923 name=volume_name, 924 changes=changes) 925 SubmitOrSend(op, opts) 926 else: 927 ToStderr("No changes to perform, exiting.")
928
929 930 -def RepairStorage(opts, args):
931 """Repairs a storage volume on a node. 932 933 @param opts: the command line options selected by the user 934 @type args: list 935 @param args: should contain 3 items: node name, storage type and volume name 936 @rtype: int 937 @return: the desired exit code 938 939 """ 940 (node_name, user_storage_type, volume_name) = args 941 942 storage_type = ConvertStorageType(user_storage_type) 943 944 op = opcodes.OpRepairNodeStorage(node_name=node_name, 945 storage_type=storage_type, 946 name=volume_name, 947 ignore_consistency=opts.ignore_consistency) 948 SubmitOrSend(op, opts)
949
950 951 -def SetNodeParams(opts, args):
952 """Modifies a node. 953 954 @param opts: the command line options selected by the user 955 @type args: list 956 @param args: should contain only one element, the node name 957 @rtype: int 958 @return: the desired exit code 959 960 """ 961 all_changes = [opts.master_candidate, opts.drained, opts.offline, 962 opts.master_capable, opts.vm_capable, opts.secondary_ip, 963 opts.ndparams] 964 if (all_changes.count(None) == len(all_changes) and 965 not (opts.hv_state or opts.disk_state)): 966 ToStderr("Please give at least one of the parameters.") 967 return 1 968 969 if opts.disk_state: 970 disk_state = utils.FlatToDict(opts.disk_state) 971 else: 972 disk_state = {} 973 974 hv_state = dict(opts.hv_state) 975 976 op = opcodes.OpNodeSetParams(node_name=args[0], 977 master_candidate=opts.master_candidate, 978 offline=opts.offline, 979 drained=opts.drained, 980 master_capable=opts.master_capable, 981 vm_capable=opts.vm_capable, 982 secondary_ip=opts.secondary_ip, 983 force=opts.force, 984 ndparams=opts.ndparams, 985 auto_promote=opts.auto_promote, 986 powered=opts.node_powered, 987 hv_state=hv_state, 988 disk_state=disk_state) 989 990 # even if here we process the result, we allow submit only 991 result = SubmitOrSend(op, opts) 992 993 if result: 994 ToStdout("Modified node %s", args[0]) 995 for param, data in result: 996 ToStdout(" - %-5s -> %s", param, data) 997 return 0
998
999 1000 -def RestrictedCommand(opts, args):
1001 """Runs a remote command on node(s). 1002 1003 @param opts: Command line options selected by user 1004 @type args: list 1005 @param args: Command line arguments 1006 @rtype: int 1007 @return: Exit code 1008 1009 """ 1010 cl = GetClient() 1011 1012 if len(args) > 1 or opts.nodegroup: 1013 # Expand node names 1014 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup) 1015 else: 1016 raise errors.OpPrereqError("Node group or node names must be given", 1017 errors.ECODE_INVAL) 1018 1019 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes, 1020 use_locking=opts.do_locking) 1021 result = SubmitOrSend(op, opts, cl=cl) 1022 1023 exit_code = constants.EXIT_SUCCESS 1024 1025 for (node, (status, text)) in zip(nodes, result): 1026 ToStdout("------------------------------------------------") 1027 if status: 1028 if opts.show_machine_names: 1029 for line in text.splitlines(): 1030 ToStdout("%s: %s", node, line) 1031 else: 1032 ToStdout("Node: %s", node) 1033 ToStdout(text) 1034 else: 1035 exit_code = constants.EXIT_FAILURE 1036 ToStdout(text) 1037 1038 return exit_code
1039
1040 1041 -class ReplyStatus(object):
1042 """Class holding a reply status for synchronous confd clients. 1043 1044 """
1045 - def __init__(self):
1046 self.failure = True 1047 self.answer = False
1048
1049 1050 -def ListDrbd(opts, args):
1051 """Modifies a node. 1052 1053 @param opts: the command line options selected by the user 1054 @type args: list 1055 @param args: should contain only one element, the node name 1056 @rtype: int 1057 @return: the desired exit code 1058 1059 """ 1060 if len(args) != 1: 1061 ToStderr("Please give one (and only one) node.") 1062 return constants.EXIT_FAILURE 1063 1064 status = ReplyStatus() 1065 1066 def ListDrbdConfdCallback(reply): 1067 """Callback for confd queries""" 1068 if reply.type == confd_client.UPCALL_REPLY: 1069 answer = reply.server_reply.answer 1070 reqtype = reply.orig_request.type 1071 if reqtype == constants.CONFD_REQ_NODE_DRBD: 1072 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK: 1073 ToStderr("Query gave non-ok status '%s': %s" % 1074 (reply.server_reply.status, 1075 reply.server_reply.answer)) 1076 status.failure = True 1077 return 1078 if not confd.HTNodeDrbd(answer): 1079 ToStderr("Invalid response from server: expected %s, got %s", 1080 confd.HTNodeDrbd, answer) 1081 status.failure = True 1082 else: 1083 status.failure = False 1084 status.answer = answer 1085 else: 1086 ToStderr("Unexpected reply %s!?", reqtype) 1087 status.failure = True
1088 1089 node = args[0] 1090 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY) 1091 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback) 1092 counting_callback = confd_client.ConfdCountingCallback(filter_callback) 1093 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST], 1094 counting_callback) 1095 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD, 1096 query=node) 1097 1098 def DoConfdRequestReply(req): 1099 counting_callback.RegisterQuery(req.rsalt) 1100 cf_client.SendRequest(req, async=False) 1101 while not counting_callback.AllAnswered(): 1102 if not cf_client.ReceiveReply(): 1103 ToStderr("Did not receive all expected confd replies") 1104 break 1105 1106 DoConfdRequestReply(req) 1107 1108 if status.failure: 1109 return constants.EXIT_FAILURE 1110 1111 fields = ["node", "minor", "instance", "disk", "role", "peer"] 1112 if opts.no_headers: 1113 headers = None 1114 else: 1115 headers = {"node": "Node", "minor": "Minor", "instance": "Instance", 1116 "disk": "Disk", "role": "Role", "peer": "PeerNode"} 1117 1118 data = GenerateTable(separator=opts.separator, headers=headers, 1119 fields=fields, data=sorted(status.answer), 1120 numfields=["minor"]) 1121 for line in data: 1122 ToStdout(line) 1123 1124 return constants.EXIT_SUCCESS 1125 1126 1127 commands = { 1128 "add": ( 1129 AddNode, [ArgHost(min=1, max=1)], 1130 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT, 1131 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, 1132 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT, 1133 DISK_STATE_OPT], 1134 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]" 1135 " [--no-node-setup] [--verbose] [--network] <node_name>", 1136 "Add a node to the cluster"), 1137 "evacuate": ( 1138 EvacuateNode, ARGS_ONE_NODE, 1139 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT, 1140 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT] + SUBMIT_OPTS, 1141 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>", 1142 "Relocate the primary and/or secondary instances from a node"), 1143 "failover": ( 1144 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, 1145 IALLOCATOR_OPT, PRIORITY_OPT], 1146 "[-f] <node>", 1147 "Stops the primary instances on a node and start them on their" 1148 " secondary node (only for instances with drbd disk template)"), 1149 "migrate": ( 1150 MigrateNode, ARGS_ONE_NODE, 1151 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT, 1152 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT, 1153 NORUNTIME_CHGS_OPT] + SUBMIT_OPTS, 1154 "[-f] <node>", 1155 "Migrate all the primary instance on a node away from it" 1156 " (only for instances of type drbd)"), 1157 "info": ( 1158 ShowNodeConfig, ARGS_MANY_NODES, [], 1159 "[<node_name>...]", "Show information about the node(s)"), 1160 "list": ( 1161 ListNodes, ARGS_MANY_NODES, 1162 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT, 1163 FORCE_FILTER_OPT], 1164 "[nodes...]", 1165 "Lists the nodes in the cluster. The available fields can be shown using" 1166 " the \"list-fields\" command (see the man page for details)." 1167 " The default field list is (in order): %s." % 1168 utils.CommaJoin(_LIST_DEF_FIELDS)), 1169 "list-fields": ( 1170 ListNodeFields, [ArgUnknown()], 1171 [NOHDR_OPT, SEP_OPT], 1172 "[fields...]", 1173 "Lists all available fields for nodes"), 1174 "modify": ( 1175 SetNodeParams, ARGS_ONE_NODE, 1176 [FORCE_OPT] + SUBMIT_OPTS + 1177 [MC_OPT, DRAINED_OPT, OFFLINE_OPT, 1178 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT, 1179 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT, 1180 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT], 1181 "<node_name>", "Alters the parameters of a node"), 1182 "powercycle": ( 1183 PowercycleNode, ARGS_ONE_NODE, 1184 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1185 "<node_name>", "Tries to forcefully powercycle a node"), 1186 "power": ( 1187 PowerNode, 1188 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS), 1189 ArgNode()], 1190 SUBMIT_OPTS + 1191 [AUTO_PROMOTE_OPT, PRIORITY_OPT, 1192 IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, 1193 POWER_DELAY_OPT], 1194 "on|off|cycle|status [nodes...]", 1195 "Change power state of node by calling out-of-band helper."), 1196 "remove": ( 1197 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT], 1198 "<node_name>", "Removes a node from the cluster"), 1199 "volumes": ( 1200 ListVolumes, [ArgNode()], 1201 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT], 1202 "[<node_name>...]", "List logical volumes on node(s)"), 1203 "list-storage": ( 1204 ListStorage, ARGS_MANY_NODES, 1205 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT, 1206 PRIORITY_OPT], 1207 "[<node_name>...]", "List physical volumes on node(s). The available" 1208 " fields are (see the man page for details): %s." % 1209 (utils.CommaJoin(_LIST_STOR_HEADERS))), 1210 "modify-storage": ( 1211 ModifyStorage, 1212 [ArgNode(min=1, max=1), 1213 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES), 1214 ArgFile(min=1, max=1)], 1215 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1216 "<node_name> <storage_type> <name>", "Modify storage volume on a node"), 1217 "repair-storage": ( 1218 RepairStorage, 1219 [ArgNode(min=1, max=1), 1220 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES), 1221 ArgFile(min=1, max=1)], 1222 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1223 "<node_name> <storage_type> <name>", 1224 "Repairs a storage volume on a node"), 1225 "list-tags": ( 1226 ListTags, ARGS_ONE_NODE, [], 1227 "<node_name>", "List the tags of the given node"), 1228 "add-tags": ( 1229 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], 1230 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1231 "<node_name> tag...", "Add tags to the given node"), 1232 "remove-tags": ( 1233 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], 1234 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS, 1235 "<node_name> tag...", "Remove tags from the given node"), 1236 "health": ( 1237 Health, ARGS_MANY_NODES, 1238 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT], 1239 "[<node_name>...]", "List health of node(s) using out-of-band"), 1240 "list-drbd": ( 1241 ListDrbd, ARGS_ONE_NODE, 1242 [NOHDR_OPT, SEP_OPT], 1243 "[<node_name>]", "Query the list of used DRBD minors on the given node"), 1244 "restricted-command": ( 1245 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES, 1246 [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT], 1247 "<command> <node_name> [<node_name>...]", 1248 "Executes a restricted command on node(s)"), 1249 } 1250 1251 #: dictionary with aliases for commands 1252 aliases = { 1253 "show": "info", 1254 }
1255 1256 1257 -def Main():
1258 return GenericMain(commands, aliases=aliases, 1259 override={"tag_type": constants.TAG_NODE}, 1260 env_override=_ENV_OVERRIDE)
1261