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