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