1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 """Node related commands"""
31
32
33
34
35
36
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
57 _LIST_DEF_FIELDS = [
58 "name", "dtotal", "dfree",
59 "mtotal", "mnode", "mfree",
60 "pinst_cnt", "sinst_cnt",
61 ]
62
63
64
65 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
66
67
68
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
81 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
82
83
84
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
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)"))
147
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
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
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
257
258
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
278 ssh_port = None
279 try:
280
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
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
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
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
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
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
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
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
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
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
501
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
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
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:
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
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
626
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
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
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
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
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
746
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
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
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
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
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
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
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
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
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
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
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
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
1074
1077 """Class holding a reply status for synchronous confd clients.
1078
1079 """
1081 self.failure = True
1082 self.answer = False
1083
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
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