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