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