Merge branch 'stable-2.15' into stable-2.16
[ganeti-github.git] / lib / client / gnt_node.py
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)