Merge branch 'stable-2.13' into stable-2.14
[ganeti-github.git] / lib / client / gnt_node.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Node related commands"""
31
32 # pylint: disable=W0401,W0613,W0614,C0103
33 # W0401: Wildcard import ganeti.cli
34 # W0613: Unused argument, since all functions follow the same API
35 # W0614: Unused import %s from wildcard import (since we need cli)
36 # C0103: Invalid name gnt-node
37
38 import itertools
39 import errno
40
41 from ganeti.cli import *
42 from ganeti import cli
43 from ganeti import bootstrap
44 from ganeti import opcodes
45 from ganeti import utils
46 from ganeti import constants
47 from ganeti import errors
48 from ganeti import netutils
49 from ganeti import pathutils
50 from ganeti import ssh
51 from ganeti import compat
52
53 from ganeti import confd
54 from ganeti.confd import client as confd_client
55
56 #: default list of field for L{ListNodes}
57 _LIST_DEF_FIELDS = [
58 "name", "dtotal", "dfree",
59 "mtotal", "mnode", "mfree",
60 "pinst_cnt", "sinst_cnt",
61 ]
62
63
64 #: Default field list for L{ListVolumes}
65 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
66
67
68 #: default list of field for L{ListStorage}
69 _LIST_STOR_DEF_FIELDS = [
70 constants.SF_NODE,
71 constants.SF_TYPE,
72 constants.SF_NAME,
73 constants.SF_SIZE,
74 constants.SF_USED,
75 constants.SF_FREE,
76 constants.SF_ALLOCATABLE,
77 ]
78
79
80 #: default list of power commands
81 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
82
83
84 #: headers (and full field list) for L{ListStorage}
85 _LIST_STOR_HEADERS = {
86 constants.SF_NODE: "Node",
87 constants.SF_TYPE: "Type",
88 constants.SF_NAME: "Name",
89 constants.SF_SIZE: "Size",
90 constants.SF_USED: "Used",
91 constants.SF_FREE: "Free",
92 constants.SF_ALLOCATABLE: "Allocatable",
93 }
94
95
96 #: User-facing storage unit types
97 _USER_STORAGE_TYPE = {
98 constants.ST_FILE: "file",
99 constants.ST_LVM_PV: "lvm-pv",
100 constants.ST_LVM_VG: "lvm-vg",
101 constants.ST_SHARED_FILE: "sharedfile",
102 constants.ST_GLUSTER: "gluster",
103 }
104
105 _STORAGE_TYPE_OPT = \
106 cli_option("-t", "--storage-type",
107 dest="user_storage_type",
108 choices=_USER_STORAGE_TYPE.keys(),
109 default=None,
110 metavar="STORAGE_TYPE",
111 help=("Storage type (%s)" %
112 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
113
114 _REPAIRABLE_STORAGE_TYPES = \
115 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
116 if constants.SO_FIX_CONSISTENCY in so]
117
118 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
119
120 _OOB_COMMAND_ASK = compat.UniqueFrozenset([
121 constants.OOB_POWER_OFF,
122 constants.OOB_POWER_CYCLE,
123 ])
124
125 _ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
126
127 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
128 action="store_false", dest="node_setup",
129 help=("Do not make initial SSH setup on remote"
130 " node (needs to be done manually)"))
131
132 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
133 action="store_true", dest="ignore_status",
134 help=("Ignore the Node(s) offline status"
135 " (potentially DANGEROUS)"))
136
137
138 def ConvertStorageType(user_storage_type):
139 """Converts a user storage type to its internal name.
140
141 """
142 try:
143 return _USER_STORAGE_TYPE[user_storage_type]
144 except KeyError:
145 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
146 errors.ECODE_INVAL)
147
148
149 def _TryReadFile(path):
150 """Tries to read a file.
151
152 If the file is not found, C{None} is returned.
153
154 @type path: string
155 @param path: Filename
156 @rtype: None or string
157 @todo: Consider adding a generic ENOENT wrapper
158
159 """
160 try:
161 return utils.ReadFile(path)
162 except EnvironmentError, err:
163 if err.errno == errno.ENOENT:
164 return None
165 else:
166 raise
167
168
169 def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
170 """Reads the DSA SSH keys according to C{keyfiles}.
171
172 @type keyfiles: dict
173 @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
174 tuples (private and public key file)
175 @rtype: list
176 @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
177 public key as strings)
178
179 """
180 result = []
181
182 for (kind, (private_file, public_file)) in keyfiles.items():
183 private_key = _TryReadFile(private_file)
184 public_key = _TryReadFile(public_file)
185
186 if public_key and private_key:
187 result.append((kind, private_key, public_key))
188 elif public_key or private_key:
189 _tostderr_fn("Couldn't find a complete set of keys for kind '%s';"
190 " files '%s' and '%s'", kind, private_file, public_file)
191
192 return result
193
194
195 def _SetupSSH(options, cluster_name, node, ssh_port, cl):
196 """Configures a destination node's SSH daemon.
197
198 @param options: Command line options
199 @type cluster_name
200 @param cluster_name: Cluster name
201 @type node: string
202 @param node: Destination node name
203 @type ssh_port: int
204 @param ssh_port: Destination node ssh port
205 @param cl: luxi client
206
207 """
208 # Retrieve the list of master and master candidates
209 candidate_filter = ["|", ["=", "role", "M"], ["=", "role", "C"]]
210 result = cl.Query(constants.QR_NODE, ["uuid"], candidate_filter)
211 if len(result.data) < 1:
212 raise errors.OpPrereqError("No master or master candidate node is found.")
213 candidates = [uuid for ((_, uuid),) in result.data]
214 candidate_keys = ssh.QueryPubKeyFile(candidates)
215
216 if options.force_join:
217 ToStderr("The \"--force-join\" option is no longer supported and will be"
218 " ignored.")
219
220 host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
221
222 (_, root_keyfiles) = \
223 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
224
225 dsa_root_keyfiles = dict((kind, value) for (kind, value)
226 in root_keyfiles.items()
227 if kind == constants.SSHK_DSA)
228 root_keys = _ReadSshKeys(dsa_root_keyfiles)
229
230 (_, cert_pem) = \
231 utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
232
233 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 # Unfortunately, we have to add the key with the node name rather than
252 # the node's UUID here, because at this point, we do not have a UUID yet.
253 # The entry will be corrected in noded later.
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 # Retrieve relevant parameters of the node group.
273 ssh_port = None
274 try:
275 # Passing [] to QueryGroups means query the default group:
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 # read the cluster name from the master
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
348
349 def ListNodes(opts, args):
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
371
372 def ListNodeFields(opts, args):
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
387
388 def EvacuateNode(opts, args):
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 # Determine affected instances
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 # No instances to evacuate
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 # Evacuate node
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 ignore_soft_errors=opts.ignore_soft_errors)
449 result = SubmitOrSend(op, opts, cl=cl)
450
451 # Keep track of submitted jobs
452 jex = JobExecutor(cl=cl, opts=opts)
453
454 for (status, job_id) in result[constants.JOB_IDS_KEY]:
455 jex.AddJobId(None, status, job_id)
456
457 results = jex.GetResults()
458 bad_cnt = len([row for row in results if not row[0]])
459 if bad_cnt == 0:
460 ToStdout("All instances evacuated successfully.")
461 rcode = constants.EXIT_SUCCESS
462 else:
463 ToStdout("There were %s errors during the evacuation.", bad_cnt)
464 rcode = constants.EXIT_FAILURE
465
466 return rcode
467
468
469 def FailoverNode(opts, args):
470 """Failover all primary instance on a node.
471
472 @param opts: the command line options selected by the user
473 @type args: list
474 @param args: should be an empty list
475 @rtype: int
476 @return: the desired exit code
477
478 """
479 cl = GetClient()
480 force = opts.force
481 selected_fields = ["name", "pinst_list"]
482
483 # these fields are static data anyway, so it doesn't matter, but
484 # locking=True should be safer
485 qcl = GetClient()
486 result = qcl.QueryNodes(names=args, fields=selected_fields,
487 use_locking=False)
488 qcl.Close()
489 node, pinst = result[0]
490
491 if not pinst:
492 ToStderr("No primary instances on node %s, exiting.", node)
493 return 0
494
495 pinst = utils.NiceSort(pinst)
496
497 retcode = 0
498
499 if not force and not AskUser("Fail over instance(s) %s?" %
500 (",".join("'%s'" % name for name in pinst))):
501 return 2
502
503 jex = JobExecutor(cl=cl, opts=opts)
504 for iname in pinst:
505 op = opcodes.OpInstanceFailover(instance_name=iname,
506 ignore_consistency=opts.ignore_consistency,
507 iallocator=opts.iallocator)
508 jex.QueueJob(iname, op)
509 results = jex.GetResults()
510 bad_cnt = len([row for row in results if not row[0]])
511 if bad_cnt == 0:
512 ToStdout("All %d instance(s) failed over successfully.", len(results))
513 else:
514 ToStdout("There were errors during the failover:\n"
515 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
516 return retcode
517
518
519 def MigrateNode(opts, args):
520 """Migrate all primary instance on a node.
521
522 """
523 cl = GetClient()
524 force = opts.force
525 selected_fields = ["name", "pinst_list"]
526
527 qcl = GetClient()
528 result = qcl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
529 qcl.Close()
530 ((node, pinst), ) = result
531
532 if not pinst:
533 ToStdout("No primary instances on node %s, exiting." % node)
534 return 0
535
536 pinst = utils.NiceSort(pinst)
537
538 if not (force or
539 AskUser("Migrate instance(s) %s?" %
540 utils.CommaJoin(utils.NiceSort(pinst)))):
541 return constants.EXIT_CONFIRMATION
542
543 # this should be removed once --non-live is deprecated
544 if not opts.live and opts.migration_mode is not None:
545 raise errors.OpPrereqError("Only one of the --non-live and "
546 "--migration-mode options can be passed",
547 errors.ECODE_INVAL)
548 if not opts.live: # --non-live passed
549 mode = constants.HT_MIGRATION_NONLIVE
550 else:
551 mode = opts.migration_mode
552
553 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
554 iallocator=opts.iallocator,
555 target_node=opts.dst_node,
556 allow_runtime_changes=opts.allow_runtime_chgs,
557 ignore_ipolicy=opts.ignore_ipolicy)
558
559 result = SubmitOrSend(op, opts, cl=cl)
560
561 # Keep track of submitted jobs
562 jex = JobExecutor(cl=cl, opts=opts)
563
564 for (status, job_id) in result[constants.JOB_IDS_KEY]:
565 jex.AddJobId(None, status, job_id)
566
567 results = jex.GetResults()
568 bad_cnt = len([row for row in results if not row[0]])
569 if bad_cnt == 0:
570 ToStdout("All instances migrated successfully.")
571 rcode = constants.EXIT_SUCCESS
572 else:
573 ToStdout("There were %s errors during the node migration.", bad_cnt)
574 rcode = constants.EXIT_FAILURE
575
576 return rcode
577
578
579 def _FormatNodeInfo(node_info):
580 """Format node information for L{cli.PrintGenericInfo()}.
581
582 """
583 (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
584 master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
585 info = [
586 ("Node name", name),
587 ("primary ip", primary_ip),
588 ("secondary ip", secondary_ip),
589 ("master candidate", is_mc),
590 ("drained", drained),
591 ("offline", offline),
592 ]
593 if powered is not None:
594 info.append(("powered", powered))
595 info.extend([
596 ("master_capable", master_capable),
597 ("vm_capable", vm_capable),
598 ])
599 if vm_capable:
600 info.extend([
601 ("primary for instances",
602 [iname for iname in utils.NiceSort(pinst)]),
603 ("secondary for instances",
604 [iname for iname in utils.NiceSort(sinst)]),
605 ])
606 info.append(("node parameters",
607 FormatParamsDictInfo(ndparams_custom, ndparams)))
608 return info
609
610
611 def ShowNodeConfig(opts, args):
612 """Show node information.
613
614 @param opts: the command line options selected by the user
615 @type args: list
616 @param args: should either be an empty list, in which case
617 we show information about all nodes, or should contain
618 a list of nodes to be queried for information
619 @rtype: int
620 @return: the desired exit code
621
622 """
623 cl = GetClient()
624 result = cl.QueryNodes(fields=["name", "pip", "sip",
625 "pinst_list", "sinst_list",
626 "master_candidate", "drained", "offline",
627 "master_capable", "vm_capable", "powered",
628 "ndparams", "custom_ndparams"],
629 names=args, use_locking=False)
630 PrintGenericInfo([
631 _FormatNodeInfo(node_info)
632 for node_info in result
633 ])
634 return 0
635
636
637 def RemoveNode(opts, args):
638 """Remove a node from the cluster.
639
640 @param opts: the command line options selected by the user
641 @type args: list
642 @param args: should contain only one element, the name of
643 the node to be removed
644 @rtype: int
645 @return: the desired exit code
646
647 """
648 op = opcodes.OpNodeRemove(node_name=args[0])
649 SubmitOpCode(op, opts=opts)
650 return 0
651
652
653 def PowercycleNode(opts, args):
654 """Remove a node from the cluster.
655
656 @param opts: the command line options selected by the user
657 @type args: list
658 @param args: should contain only one element, the name of
659 the node to be removed
660 @rtype: int
661 @return: the desired exit code
662
663 """
664 node = args[0]
665 if (not opts.confirm and
666 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
667 return 2
668
669 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
670 result = SubmitOrSend(op, opts)
671 if result:
672 ToStderr(result)
673 return 0
674
675
676 def PowerNode(opts, args):
677 """Change/ask power state of a node.
678
679 @param opts: the command line options selected by the user
680 @type args: list
681 @param args: should contain only one element, the name of
682 the node to be removed
683 @rtype: int
684 @return: the desired exit code
685
686 """
687 command = args.pop(0)
688
689 if opts.no_headers:
690 headers = None
691 else:
692 headers = {"node": "Node", "status": "Status"}
693
694 if command not in _LIST_POWER_COMMANDS:
695 ToStderr("power subcommand %s not supported." % command)
696 return constants.EXIT_FAILURE
697
698 oob_command = "power-%s" % command
699
700 if oob_command in _OOB_COMMAND_ASK:
701 if not args:
702 ToStderr("Please provide at least one node for this command")
703 return constants.EXIT_FAILURE
704 elif not opts.force and not ConfirmOperation(args, "nodes",
705 "power %s" % command):
706 return constants.EXIT_FAILURE
707 assert len(args) > 0
708
709 opcodelist = []
710 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
711 # TODO: This is a little ugly as we can't catch and revert
712 for node in args:
713 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
714 auto_promote=opts.auto_promote))
715
716 opcodelist.append(opcodes.OpOobCommand(node_names=args,
717 command=oob_command,
718 ignore_status=opts.ignore_status,
719 timeout=opts.oob_timeout,
720 power_delay=opts.power_delay))
721
722 cli.SetGenericOpcodeOpts(opcodelist, opts)
723
724 job_id = cli.SendJob(opcodelist)
725
726 # We just want the OOB Opcode status
727 # If it fails PollJob gives us the error message in it
728 result = cli.PollJob(job_id)[-1]
729
730 errs = 0
731 data = []
732 for node_result in result:
733 (node_tuple, data_tuple) = node_result
734 (_, node_name) = node_tuple
735 (data_status, data_node) = data_tuple
736 if data_status == constants.RS_NORMAL:
737 if oob_command == constants.OOB_POWER_STATUS:
738 if data_node[constants.OOB_POWER_STATUS_POWERED]:
739 text = "powered"
740 else:
741 text = "unpowered"
742 data.append([node_name, text])
743 else:
744 # We don't expect data here, so we just say, it was successfully invoked
745 data.append([node_name, "invoked"])
746 else:
747 errs += 1
748 data.append([node_name, cli.FormatResultError(data_status, True)])
749
750 data = GenerateTable(separator=opts.separator, headers=headers,
751 fields=["node", "status"], data=data)
752
753 for line in data:
754 ToStdout(line)
755
756 if errs:
757 return constants.EXIT_FAILURE
758 else:
759 return constants.EXIT_SUCCESS
760
761
762 def Health(opts, args):
763 """Show health of a node using OOB.
764
765 @param opts: the command line options selected by the user
766 @type args: list
767 @param args: should contain only one element, the name of
768 the node to be removed
769 @rtype: int
770 @return: the desired exit code
771
772 """
773 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
774 timeout=opts.oob_timeout)
775 result = SubmitOpCode(op, opts=opts)
776
777 if opts.no_headers:
778 headers = None
779 else:
780 headers = {"node": "Node", "status": "Status"}
781
782 errs = 0
783 data = []
784 for node_result in result:
785 (node_tuple, data_tuple) = node_result
786 (_, node_name) = node_tuple
787 (data_status, data_node) = data_tuple
788 if data_status == constants.RS_NORMAL:
789 data.append([node_name, "%s=%s" % tuple(data_node[0])])
790 for item, status in data_node[1:]:
791 data.append(["", "%s=%s" % (item, status)])
792 else:
793 errs += 1
794 data.append([node_name, cli.FormatResultError(data_status, True)])
795
796 data = GenerateTable(separator=opts.separator, headers=headers,
797 fields=["node", "status"], data=data)
798
799 for line in data:
800 ToStdout(line)
801
802 if errs:
803 return constants.EXIT_FAILURE
804 else:
805 return constants.EXIT_SUCCESS
806
807
808 def ListVolumes(opts, args):
809 """List logical volumes on node(s).
810
811 @param opts: the command line options selected by the user
812 @type args: list
813 @param args: should either be an empty list, in which case
814 we list data for all nodes, or contain a list of nodes
815 to display data only for those
816 @rtype: int
817 @return: the desired exit code
818
819 """
820 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
821
822 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
823 output = SubmitOpCode(op, opts=opts)
824
825 if not opts.no_headers:
826 headers = {"node": "Node", "phys": "PhysDev",
827 "vg": "VG", "name": "Name",
828 "size": "Size", "instance": "Instance"}
829 else:
830 headers = None
831
832 unitfields = ["size"]
833
834 numfields = ["size"]
835
836 data = GenerateTable(separator=opts.separator, headers=headers,
837 fields=selected_fields, unitfields=unitfields,
838 numfields=numfields, data=output, units=opts.units)
839
840 for line in data:
841 ToStdout(line)
842
843 return 0
844
845
846 def ListStorage(opts, args):
847 """List physical volumes on node(s).
848
849 @param opts: the command line options selected by the user
850 @type args: list
851 @param args: should either be an empty list, in which case
852 we list data for all nodes, or contain a list of nodes
853 to display data only for those
854 @rtype: int
855 @return: the desired exit code
856
857 """
858 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
859
860 op = opcodes.OpNodeQueryStorage(nodes=args,
861 storage_type=opts.user_storage_type,
862 output_fields=selected_fields)
863 output = SubmitOpCode(op, opts=opts)
864
865 if not opts.no_headers:
866 headers = {
867 constants.SF_NODE: "Node",
868 constants.SF_TYPE: "Type",
869 constants.SF_NAME: "Name",
870 constants.SF_SIZE: "Size",
871 constants.SF_USED: "Used",
872 constants.SF_FREE: "Free",
873 constants.SF_ALLOCATABLE: "Allocatable",
874 }
875 else:
876 headers = None
877
878 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
879 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
880
881 # change raw values to nicer strings
882 for row in output:
883 for idx, field in enumerate(selected_fields):
884 val = row[idx]
885 if field == constants.SF_ALLOCATABLE:
886 if val:
887 val = "Y"
888 else:
889 val = "N"
890 row[idx] = str(val)
891
892 data = GenerateTable(separator=opts.separator, headers=headers,
893 fields=selected_fields, unitfields=unitfields,
894 numfields=numfields, data=output, units=opts.units)
895
896 for line in data:
897 ToStdout(line)
898
899 return 0
900
901
902 def ModifyStorage(opts, args):
903 """Modify 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 changes = {}
917
918 if opts.allocatable is not None:
919 changes[constants.SF_ALLOCATABLE] = opts.allocatable
920
921 if changes:
922 op = opcodes.OpNodeModifyStorage(node_name=node_name,
923 storage_type=storage_type,
924 name=volume_name,
925 changes=changes)
926 SubmitOrSend(op, opts)
927 else:
928 ToStderr("No changes to perform, exiting.")
929
930
931 def RepairStorage(opts, args):
932 """Repairs a storage volume on a node.
933
934 @param opts: the command line options selected by the user
935 @type args: list
936 @param args: should contain 3 items: node name, storage type and volume name
937 @rtype: int
938 @return: the desired exit code
939
940 """
941 (node_name, user_storage_type, volume_name) = args
942
943 storage_type = ConvertStorageType(user_storage_type)
944
945 op = opcodes.OpRepairNodeStorage(node_name=node_name,
946 storage_type=storage_type,
947 name=volume_name,
948 ignore_consistency=opts.ignore_consistency)
949 SubmitOrSend(op, opts)
950
951
952 def SetNodeParams(opts, args):
953 """Modifies a node.
954
955 @param opts: the command line options selected by the user
956 @type args: list
957 @param args: should contain only one element, the node name
958 @rtype: int
959 @return: the desired exit code
960
961 """
962 all_changes = [opts.master_candidate, opts.drained, opts.offline,
963 opts.master_capable, opts.vm_capable, opts.secondary_ip,
964 opts.ndparams]
965 if (all_changes.count(None) == len(all_changes) and
966 not (opts.hv_state or opts.disk_state)):
967 ToStderr("Please give at least one of the parameters.")
968 return 1
969
970 if opts.disk_state:
971 disk_state = utils.FlatToDict(opts.disk_state)
972 else:
973 disk_state = {}
974
975 hv_state = dict(opts.hv_state)
976
977 op = opcodes.OpNodeSetParams(node_name=args[0],
978 master_candidate=opts.master_candidate,
979 offline=opts.offline,
980 drained=opts.drained,
981 master_capable=opts.master_capable,
982 vm_capable=opts.vm_capable,
983 secondary_ip=opts.secondary_ip,
984 force=opts.force,
985 ndparams=opts.ndparams,
986 auto_promote=opts.auto_promote,
987 powered=opts.node_powered,
988 hv_state=hv_state,
989 disk_state=disk_state)
990
991 # even if here we process the result, we allow submit only
992 result = SubmitOrSend(op, opts)
993
994 if result:
995 ToStdout("Modified node %s", args[0])
996 for param, data in result:
997 ToStdout(" - %-5s -> %s", param, data)
998 return 0
999
1000
1001 def RestrictedCommand(opts, args):
1002 """Runs a remote command on node(s).
1003
1004 @param opts: Command line options selected by user
1005 @type args: list
1006 @param args: Command line arguments
1007 @rtype: int
1008 @return: Exit code
1009
1010 """
1011 cl = GetClient()
1012
1013 if len(args) > 1 or opts.nodegroup:
1014 # Expand node names
1015 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
1016 else:
1017 raise errors.OpPrereqError("Node group or node names must be given",
1018 errors.ECODE_INVAL)
1019
1020 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
1021 use_locking=opts.do_locking)
1022 result = SubmitOrSend(op, opts, cl=cl)
1023
1024 exit_code = constants.EXIT_SUCCESS
1025
1026 for (node, (status, text)) in zip(nodes, result):
1027 ToStdout("------------------------------------------------")
1028 if status:
1029 if opts.show_machine_names:
1030 for line in text.splitlines():
1031 ToStdout("%s: %s", node, line)
1032 else:
1033 ToStdout("Node: %s", node)
1034 ToStdout(text)
1035 else:
1036 exit_code = constants.EXIT_FAILURE
1037 ToStdout(text)
1038
1039 return exit_code
1040
1041
1042 class ReplyStatus(object):
1043 """Class holding a reply status for synchronous confd clients.
1044
1045 """
1046 def __init__(self):
1047 self.failure = True
1048 self.answer = False
1049
1050
1051 def ListDrbd(opts, args):
1052 """Modifies a node.
1053
1054 @param opts: the command line options selected by the user
1055 @type args: list
1056 @param args: should contain only one element, the node name
1057 @rtype: int
1058 @return: the desired exit code
1059
1060 """
1061 if len(args) != 1:
1062 ToStderr("Please give one (and only one) node.")
1063 return constants.EXIT_FAILURE
1064
1065 status = ReplyStatus()
1066
1067 def ListDrbdConfdCallback(reply):
1068 """Callback for confd queries"""
1069 if reply.type == confd_client.UPCALL_REPLY:
1070 answer = reply.server_reply.answer
1071 reqtype = reply.orig_request.type
1072 if reqtype == constants.CONFD_REQ_NODE_DRBD:
1073 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1074 ToStderr("Query gave non-ok status '%s': %s" %
1075 (reply.server_reply.status,
1076 reply.server_reply.answer))
1077 status.failure = True
1078 return
1079 if not confd.HTNodeDrbd(answer):
1080 ToStderr("Invalid response from server: expected %s, got %s",
1081 confd.HTNodeDrbd, answer)
1082 status.failure = True
1083 else:
1084 status.failure = False
1085 status.answer = answer
1086 else:
1087 ToStderr("Unexpected reply %s!?", reqtype)
1088 status.failure = True
1089
1090 node = args[0]
1091 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1092 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1093 counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1094 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1095 counting_callback)
1096 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1097 query=node)
1098
1099 def DoConfdRequestReply(req):
1100 counting_callback.RegisterQuery(req.rsalt)
1101 cf_client.SendRequest(req, async=False)
1102 while not counting_callback.AllAnswered():
1103 if not cf_client.ReceiveReply():
1104 ToStderr("Did not receive all expected confd replies")
1105 break
1106
1107 DoConfdRequestReply(req)
1108
1109 if status.failure:
1110 return constants.EXIT_FAILURE
1111
1112 fields = ["node", "minor", "instance", "disk", "role", "peer"]
1113 if opts.no_headers:
1114 headers = None
1115 else:
1116 headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1117 "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1118
1119 data = GenerateTable(separator=opts.separator, headers=headers,
1120 fields=fields, data=sorted(status.answer),
1121 numfields=["minor"])
1122 for line in data:
1123 ToStdout(line)
1124
1125 return constants.EXIT_SUCCESS
1126
1127
1128 commands = {
1129 "add": (
1130 AddNode, [ArgHost(min=1, max=1)],
1131 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
1132 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
1133 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
1134 DISK_STATE_OPT],
1135 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
1136 " [--no-node-setup] [--verbose] [--network] <node_name>",
1137 "Add a node to the cluster"),
1138 "evacuate": (
1139 EvacuateNode, ARGS_ONE_NODE,
1140 [FORCE_OPT, IALLOCATOR_OPT, IGNORE_SOFT_ERRORS_OPT, NEW_SECONDARY_OPT,
1141 EARLY_RELEASE_OPT, PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT]
1142 + SUBMIT_OPTS,
1143 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
1144 "Relocate the primary and/or secondary instances from a node"),
1145 "failover": (
1146 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
1147 IALLOCATOR_OPT, PRIORITY_OPT],
1148 "[-f] <node>",
1149 "Stops the primary instances on a node and start them on their"
1150 " secondary node (only for instances with drbd disk template)"),
1151 "migrate": (
1152 MigrateNode, ARGS_ONE_NODE,
1153 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1154 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1155 NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
1156 "[-f] <node>",
1157 "Migrate all the primary instance on a node away from it"
1158 " (only for instances of type drbd)"),
1159 "info": (
1160 ShowNodeConfig, ARGS_MANY_NODES, [],
1161 "[<node_name>...]", "Show information about the node(s)"),
1162 "list": (
1163 ListNodes, ARGS_MANY_NODES,
1164 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1165 FORCE_FILTER_OPT],
1166 "[nodes...]",
1167 "Lists the nodes in the cluster. The available fields can be shown using"
1168 " the \"list-fields\" command (see the man page for details)."
1169 " The default field list is (in order): %s." %
1170 utils.CommaJoin(_LIST_DEF_FIELDS)),
1171 "list-fields": (
1172 ListNodeFields, [ArgUnknown()],
1173 [NOHDR_OPT, SEP_OPT],
1174 "[fields...]",
1175 "Lists all available fields for nodes"),
1176 "modify": (
1177 SetNodeParams, ARGS_ONE_NODE,
1178 [FORCE_OPT] + SUBMIT_OPTS +
1179 [MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1180 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1181 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1182 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1183 "<node_name>", "Alters the parameters of a node"),
1184 "powercycle": (
1185 PowercycleNode, ARGS_ONE_NODE,
1186 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1187 "<node_name>", "Tries to forcefully powercycle a node"),
1188 "power": (
1189 PowerNode,
1190 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1191 ArgNode()],
1192 SUBMIT_OPTS +
1193 [AUTO_PROMOTE_OPT, PRIORITY_OPT,
1194 IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT,
1195 POWER_DELAY_OPT],
1196 "on|off|cycle|status [nodes...]",
1197 "Change power state of node by calling out-of-band helper."),
1198 "remove": (
1199 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1200 "<node_name>", "Removes a node from the cluster"),
1201 "volumes": (
1202 ListVolumes, [ArgNode()],
1203 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1204 "[<node_name>...]", "List logical volumes on node(s)"),
1205 "list-storage": (
1206 ListStorage, ARGS_MANY_NODES,
1207 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1208 PRIORITY_OPT],
1209 "[<node_name>...]", "List physical volumes on node(s). The available"
1210 " fields are (see the man page for details): %s." %
1211 (utils.CommaJoin(_LIST_STOR_HEADERS))),
1212 "modify-storage": (
1213 ModifyStorage,
1214 [ArgNode(min=1, max=1),
1215 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1216 ArgFile(min=1, max=1)],
1217 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1218 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1219 "repair-storage": (
1220 RepairStorage,
1221 [ArgNode(min=1, max=1),
1222 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1223 ArgFile(min=1, max=1)],
1224 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1225 "<node_name> <storage_type> <name>",
1226 "Repairs a storage volume on a node"),
1227 "list-tags": (
1228 ListTags, ARGS_ONE_NODE, [],
1229 "<node_name>", "List the tags of the given node"),
1230 "add-tags": (
1231 AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1232 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1233 "<node_name> tag...", "Add tags to the given node"),
1234 "remove-tags": (
1235 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1236 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1237 "<node_name> tag...", "Remove tags from the given node"),
1238 "health": (
1239 Health, ARGS_MANY_NODES,
1240 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1241 "[<node_name>...]", "List health of node(s) using out-of-band"),
1242 "list-drbd": (
1243 ListDrbd, ARGS_ONE_NODE,
1244 [NOHDR_OPT, SEP_OPT],
1245 "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1246 "restricted-command": (
1247 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
1248 [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT],
1249 "<command> <node_name> [<node_name>...]",
1250 "Executes a restricted command on node(s)"),
1251 }
1252
1253 #: dictionary with aliases for commands
1254 aliases = {
1255 "show": "info",
1256 }
1257
1258
1259 def Main():
1260 return GenericMain(commands, aliases=aliases,
1261 override={"tag_type": constants.TAG_NODE},
1262 env_override=_ENV_OVERRIDE)