52d5bd1ff899d778c875e9bfa88b45db5e2c9233
[ganeti-github.git] / lib / client / gnt_instance.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """Instance related commands"""
22
23 # pylint: disable=W0401,W0614,C0103
24 # W0401: Wildcard import ganeti.cli
25 # W0614: Unused import %s from wildcard import (since we need cli)
26 # C0103: Invalid name gnt-instance
27
28 import copy
29 import itertools
30 import simplejson
31 import logging
32
33 from ganeti.cli import *
34 from ganeti import opcodes
35 from ganeti import constants
36 from ganeti import compat
37 from ganeti import utils
38 from ganeti import errors
39 from ganeti import netutils
40 from ganeti import ssh
41 from ganeti import objects
42 from ganeti import ht
43
44
45 _EXPAND_CLUSTER = "cluster"
46 _EXPAND_NODES_BOTH = "nodes"
47 _EXPAND_NODES_PRI = "nodes-pri"
48 _EXPAND_NODES_SEC = "nodes-sec"
49 _EXPAND_NODES_BOTH_BY_TAGS = "nodes-by-tags"
50 _EXPAND_NODES_PRI_BY_TAGS = "nodes-pri-by-tags"
51 _EXPAND_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
52 _EXPAND_INSTANCES = "instances"
53 _EXPAND_INSTANCES_BY_TAGS = "instances-by-tags"
54
55 _EXPAND_NODES_TAGS_MODES = compat.UniqueFrozenset([
56 _EXPAND_NODES_BOTH_BY_TAGS,
57 _EXPAND_NODES_PRI_BY_TAGS,
58 _EXPAND_NODES_SEC_BY_TAGS,
59 ])
60
61 #: default list of options for L{ListInstances}
62 _LIST_DEF_FIELDS = [
63 "name", "hypervisor", "os", "pnode", "status", "oper_ram",
64 ]
65
66 _MISSING = object()
67 _ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
68
69 _INST_DATA_VAL = ht.TListOf(ht.TDict)
70
71
72 def _ExpandMultiNames(mode, names, client=None):
73 """Expand the given names using the passed mode.
74
75 For _EXPAND_CLUSTER, all instances will be returned. For
76 _EXPAND_NODES_PRI/SEC, all instances having those nodes as
77 primary/secondary will be returned. For _EXPAND_NODES_BOTH, all
78 instances having those nodes as either primary or secondary will be
79 returned. For _EXPAND_INSTANCES, the given instances will be
80 returned.
81
82 @param mode: one of L{_EXPAND_CLUSTER}, L{_EXPAND_NODES_BOTH},
83 L{_EXPAND_NODES_PRI}, L{_EXPAND_NODES_SEC} or
84 L{_EXPAND_INSTANCES}
85 @param names: a list of names; for cluster, it must be empty,
86 and for node and instance it must be a list of valid item
87 names (short names are valid as usual, e.g. node1 instead of
88 node1.example.com)
89 @rtype: list
90 @return: the list of names after the expansion
91 @raise errors.ProgrammerError: for unknown selection type
92 @raise errors.OpPrereqError: for invalid input parameters
93
94 """
95 # pylint: disable=W0142
96
97 if client is None:
98 client = GetClient()
99 if mode == _EXPAND_CLUSTER:
100 if names:
101 raise errors.OpPrereqError("Cluster filter mode takes no arguments",
102 errors.ECODE_INVAL)
103 idata = client.QueryInstances([], ["name"], False)
104 inames = [row[0] for row in idata]
105
106 elif (mode in _EXPAND_NODES_TAGS_MODES or
107 mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_PRI, _EXPAND_NODES_SEC)):
108 if mode in _EXPAND_NODES_TAGS_MODES:
109 if not names:
110 raise errors.OpPrereqError("No node tags passed", errors.ECODE_INVAL)
111 ndata = client.QueryNodes([], ["name", "pinst_list",
112 "sinst_list", "tags"], False)
113 ndata = [row for row in ndata if set(row[3]).intersection(names)]
114 else:
115 if not names:
116 raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
117 ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
118 False)
119
120 ipri = [row[1] for row in ndata]
121 pri_names = list(itertools.chain(*ipri))
122 isec = [row[2] for row in ndata]
123 sec_names = list(itertools.chain(*isec))
124 if mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_BOTH_BY_TAGS):
125 inames = pri_names + sec_names
126 elif mode in (_EXPAND_NODES_PRI, _EXPAND_NODES_PRI_BY_TAGS):
127 inames = pri_names
128 elif mode in (_EXPAND_NODES_SEC, _EXPAND_NODES_SEC_BY_TAGS):
129 inames = sec_names
130 else:
131 raise errors.ProgrammerError("Unhandled shutdown type")
132 elif mode == _EXPAND_INSTANCES:
133 if not names:
134 raise errors.OpPrereqError("No instance names passed",
135 errors.ECODE_INVAL)
136 idata = client.QueryInstances(names, ["name"], False)
137 inames = [row[0] for row in idata]
138 elif mode == _EXPAND_INSTANCES_BY_TAGS:
139 if not names:
140 raise errors.OpPrereqError("No instance tags passed",
141 errors.ECODE_INVAL)
142 idata = client.QueryInstances([], ["name", "tags"], False)
143 inames = [row[0] for row in idata if set(row[1]).intersection(names)]
144 else:
145 raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
146
147 return inames
148
149
150 def _EnsureInstancesExist(client, names):
151 """Check for and ensure the given instance names exist.
152
153 This function will raise an OpPrereqError in case they don't
154 exist. Otherwise it will exit cleanly.
155
156 @type client: L{ganeti.luxi.Client}
157 @param client: the client to use for the query
158 @type names: list
159 @param names: the list of instance names to query
160 @raise errors.OpPrereqError: in case any instance is missing
161
162 """
163 # TODO: change LUInstanceQuery to that it actually returns None
164 # instead of raising an exception, or devise a better mechanism
165 result = client.QueryInstances(names, ["name"], False)
166 for orig_name, row in zip(names, result):
167 if row[0] is None:
168 raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
169 errors.ECODE_NOENT)
170
171
172 def GenericManyOps(operation, fn):
173 """Generic multi-instance operations.
174
175 The will return a wrapper that processes the options and arguments
176 given, and uses the passed function to build the opcode needed for
177 the specific operation. Thus all the generic loop/confirmation code
178 is abstracted into this function.
179
180 """
181 def realfn(opts, args):
182 if opts.multi_mode is None:
183 opts.multi_mode = _EXPAND_INSTANCES
184 cl = GetClient()
185 inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
186 if not inames:
187 if opts.multi_mode == _EXPAND_CLUSTER:
188 ToStdout("Cluster is empty, no instances to shutdown")
189 return 0
190 raise errors.OpPrereqError("Selection filter does not match"
191 " any instances", errors.ECODE_INVAL)
192 multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
193 if not (opts.force_multi or not multi_on
194 or ConfirmOperation(inames, "instances", operation)):
195 return 1
196 jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
197 for name in inames:
198 op = fn(name, opts)
199 jex.QueueJob(name, op)
200 results = jex.WaitOrShow(not opts.submit_only)
201 rcode = compat.all(row[0] for row in results)
202 return int(not rcode)
203 return realfn
204
205
206 def ListInstances(opts, args):
207 """List instances and their properties.
208
209 @param opts: the command line options selected by the user
210 @type args: list
211 @param args: should be an empty list
212 @rtype: int
213 @return: the desired exit code
214
215 """
216 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
217
218 fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
219 "nic.modes", "nic.links", "nic.bridges",
220 "nic.networks",
221 "snodes", "snodes.group", "snodes.group.uuid"],
222 (lambda value: ",".join(str(item)
223 for item in value),
224 False))
225
226 return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
227 opts.separator, not opts.no_headers,
228 format_override=fmtoverride, verbose=opts.verbose,
229 force_filter=opts.force_filter)
230
231
232 def ListInstanceFields(opts, args):
233 """List instance fields.
234
235 @param opts: the command line options selected by the user
236 @type args: list
237 @param args: fields to list, or empty for all
238 @rtype: int
239 @return: the desired exit code
240
241 """
242 return GenericListFields(constants.QR_INSTANCE, args, opts.separator,
243 not opts.no_headers)
244
245
246 def AddInstance(opts, args):
247 """Add an instance to the cluster.
248
249 This is just a wrapper over GenericInstanceCreate.
250
251 """
252 return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
253
254
255 def BatchCreate(opts, args):
256 """Create instances using a definition file.
257
258 This function reads a json file with L{opcodes.OpInstanceCreate}
259 serialisations.
260
261 @param opts: the command line options selected by the user
262 @type args: list
263 @param args: should contain one element, the json filename
264 @rtype: int
265 @return: the desired exit code
266
267 """
268 (json_filename,) = args
269 cl = GetClient()
270
271 try:
272 instance_data = simplejson.loads(utils.ReadFile(json_filename))
273 except Exception, err: # pylint: disable=W0703
274 ToStderr("Can't parse the instance definition file: %s" % str(err))
275 return 1
276
277 if not _INST_DATA_VAL(instance_data):
278 ToStderr("The instance definition file is not %s" % _INST_DATA_VAL)
279 return 1
280
281 instances = []
282 possible_params = set(opcodes.OpInstanceCreate.GetAllSlots())
283 for (idx, inst) in enumerate(instance_data):
284 unknown = set(inst.keys()) - possible_params
285
286 if unknown:
287 # TODO: Suggest closest match for more user friendly experience
288 raise errors.OpPrereqError("Unknown fields in definition %s: %s" %
289 (idx, utils.CommaJoin(unknown)),
290 errors.ECODE_INVAL)
291
292 op = opcodes.OpInstanceCreate(**inst) # pylint: disable=W0142
293 op.Validate(False)
294 instances.append(op)
295
296 op = opcodes.OpInstanceMultiAlloc(iallocator=opts.iallocator,
297 instances=instances)
298 result = SubmitOrSend(op, opts, cl=cl)
299
300 # Keep track of submitted jobs
301 jex = JobExecutor(cl=cl, opts=opts)
302
303 for (status, job_id) in result[constants.JOB_IDS_KEY]:
304 jex.AddJobId(None, status, job_id)
305
306 results = jex.GetResults()
307 bad_cnt = len([row for row in results if not row[0]])
308 if bad_cnt == 0:
309 ToStdout("All instances created successfully.")
310 rcode = constants.EXIT_SUCCESS
311 else:
312 ToStdout("There were %s errors during the creation.", bad_cnt)
313 rcode = constants.EXIT_FAILURE
314
315 return rcode
316
317
318 def ReinstallInstance(opts, args):
319 """Reinstall an instance.
320
321 @param opts: the command line options selected by the user
322 @type args: list
323 @param args: should contain only one element, the name of the
324 instance to be reinstalled
325 @rtype: int
326 @return: the desired exit code
327
328 """
329 # first, compute the desired name list
330 if opts.multi_mode is None:
331 opts.multi_mode = _EXPAND_INSTANCES
332
333 inames = _ExpandMultiNames(opts.multi_mode, args)
334 if not inames:
335 raise errors.OpPrereqError("Selection filter does not match any instances",
336 errors.ECODE_INVAL)
337
338 # second, if requested, ask for an OS
339 if opts.select_os is True:
340 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
341 result = SubmitOpCode(op, opts=opts)
342
343 if not result:
344 ToStdout("Can't get the OS list")
345 return 1
346
347 ToStdout("Available OS templates:")
348 number = 0
349 choices = []
350 for (name, variants) in result:
351 for entry in CalculateOSNames(name, variants):
352 ToStdout("%3s: %s", number, entry)
353 choices.append(("%s" % number, entry, entry))
354 number += 1
355
356 choices.append(("x", "exit", "Exit gnt-instance reinstall"))
357 selected = AskUser("Enter OS template number (or x to abort):",
358 choices)
359
360 if selected == "exit":
361 ToStderr("User aborted reinstall, exiting")
362 return 1
363
364 os_name = selected
365 os_msg = "change the OS to '%s'" % selected
366 else:
367 os_name = opts.os
368 if opts.os is not None:
369 os_msg = "change the OS to '%s'" % os_name
370 else:
371 os_msg = "keep the same OS"
372
373 # third, get confirmation: multi-reinstall requires --force-multi,
374 # single-reinstall either --force or --force-multi (--force-multi is
375 # a stronger --force)
376 multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
377 if multi_on:
378 warn_msg = ("Note: this will remove *all* data for the"
379 " below instances! It will %s.\n" % os_msg)
380 if not (opts.force_multi or
381 ConfirmOperation(inames, "instances", "reinstall", extra=warn_msg)):
382 return 1
383 else:
384 if not (opts.force or opts.force_multi):
385 usertext = ("This will reinstall the instance '%s' (and %s) which"
386 " removes all data. Continue?") % (inames[0], os_msg)
387 if not AskUser(usertext):
388 return 1
389
390 jex = JobExecutor(verbose=multi_on, opts=opts)
391 for instance_name in inames:
392 op = opcodes.OpInstanceReinstall(instance_name=instance_name,
393 os_type=os_name,
394 force_variant=opts.force_variant,
395 osparams=opts.osparams)
396 jex.QueueJob(instance_name, op)
397
398 results = jex.WaitOrShow(not opts.submit_only)
399
400 if compat.all(map(compat.fst, results)):
401 return constants.EXIT_SUCCESS
402 else:
403 return constants.EXIT_FAILURE
404
405
406 def RemoveInstance(opts, args):
407 """Remove an instance.
408
409 @param opts: the command line options selected by the user
410 @type args: list
411 @param args: should contain only one element, the name of
412 the instance to be removed
413 @rtype: int
414 @return: the desired exit code
415
416 """
417 instance_name = args[0]
418 force = opts.force
419 cl = GetClient()
420
421 if not force:
422 _EnsureInstancesExist(cl, [instance_name])
423
424 usertext = ("This will remove the volumes of the instance %s"
425 " (including mirrors), thus removing all the data"
426 " of the instance. Continue?") % instance_name
427 if not AskUser(usertext):
428 return 1
429
430 op = opcodes.OpInstanceRemove(instance_name=instance_name,
431 ignore_failures=opts.ignore_failures,
432 shutdown_timeout=opts.shutdown_timeout)
433 SubmitOrSend(op, opts, cl=cl)
434 return 0
435
436
437 def RenameInstance(opts, args):
438 """Rename an instance.
439
440 @param opts: the command line options selected by the user
441 @type args: list
442 @param args: should contain two elements, the old and the
443 new instance names
444 @rtype: int
445 @return: the desired exit code
446
447 """
448 if not opts.name_check:
449 if not AskUser("As you disabled the check of the DNS entry, please verify"
450 " that '%s' is a FQDN. Continue?" % args[1]):
451 return 1
452
453 op = opcodes.OpInstanceRename(instance_name=args[0],
454 new_name=args[1],
455 ip_check=opts.ip_check,
456 name_check=opts.name_check)
457 result = SubmitOrSend(op, opts)
458
459 if result:
460 ToStdout("Instance '%s' renamed to '%s'", args[0], result)
461
462 return 0
463
464
465 def ActivateDisks(opts, args):
466 """Activate an instance's disks.
467
468 This serves two purposes:
469 - it allows (as long as the instance is not running)
470 mounting the disks and modifying them from the node
471 - it repairs inactive secondary drbds
472
473 @param opts: the command line options selected by the user
474 @type args: list
475 @param args: should contain only one element, the instance name
476 @rtype: int
477 @return: the desired exit code
478
479 """
480 instance_name = args[0]
481 op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
482 ignore_size=opts.ignore_size,
483 wait_for_sync=opts.wait_for_sync)
484 disks_info = SubmitOrSend(op, opts)
485 for host, iname, nname in disks_info:
486 ToStdout("%s:%s:%s", host, iname, nname)
487 return 0
488
489
490 def DeactivateDisks(opts, args):
491 """Deactivate an instance's disks.
492
493 This function takes the instance name, looks for its primary node
494 and the tries to shutdown its block devices on that node.
495
496 @param opts: the command line options selected by the user
497 @type args: list
498 @param args: should contain only one element, the instance name
499 @rtype: int
500 @return: the desired exit code
501
502 """
503 instance_name = args[0]
504 op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
505 force=opts.force)
506 SubmitOrSend(op, opts)
507 return 0
508
509
510 def RecreateDisks(opts, args):
511 """Recreate an instance's disks.
512
513 @param opts: the command line options selected by the user
514 @type args: list
515 @param args: should contain only one element, the instance name
516 @rtype: int
517 @return: the desired exit code
518
519 """
520 instance_name = args[0]
521
522 disks = []
523
524 if opts.disks:
525 for didx, ddict in opts.disks:
526 didx = int(didx)
527
528 if not ht.TDict(ddict):
529 msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
530 raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
531
532 if constants.IDISK_SIZE in ddict:
533 try:
534 ddict[constants.IDISK_SIZE] = \
535 utils.ParseUnit(ddict[constants.IDISK_SIZE])
536 except ValueError, err:
537 raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
538 (didx, err), errors.ECODE_INVAL)
539
540 disks.append((didx, ddict))
541
542 # TODO: Verify modifyable parameters (already done in
543 # LUInstanceRecreateDisks, but it'd be nice to have in the client)
544
545 if opts.node:
546 if opts.iallocator:
547 msg = "At most one of either --nodes or --iallocator can be passed"
548 raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
549 pnode, snode = SplitNodeOption(opts.node)
550 nodes = [pnode]
551 if snode is not None:
552 nodes.append(snode)
553 else:
554 nodes = []
555
556 op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
557 disks=disks, nodes=nodes,
558 iallocator=opts.iallocator)
559 SubmitOrSend(op, opts)
560
561 return 0
562
563
564 def GrowDisk(opts, args):
565 """Grow an instance's disks.
566
567 @param opts: the command line options selected by the user
568 @type args: list
569 @param args: should contain three elements, the target instance name,
570 the target disk id, and the target growth
571 @rtype: int
572 @return: the desired exit code
573
574 """
575 instance = args[0]
576 disk = args[1]
577 try:
578 disk = int(disk)
579 except (TypeError, ValueError), err:
580 raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
581 errors.ECODE_INVAL)
582 try:
583 amount = utils.ParseUnit(args[2])
584 except errors.UnitParseError:
585 raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
586 errors.ECODE_INVAL)
587 op = opcodes.OpInstanceGrowDisk(instance_name=instance,
588 disk=disk, amount=amount,
589 wait_for_sync=opts.wait_for_sync,
590 absolute=opts.absolute)
591 SubmitOrSend(op, opts)
592 return 0
593
594
595 def _StartupInstance(name, opts):
596 """Startup instances.
597
598 This returns the opcode to start an instance, and its decorator will
599 wrap this into a loop starting all desired instances.
600
601 @param name: the name of the instance to act on
602 @param opts: the command line options selected by the user
603 @return: the opcode needed for the operation
604
605 """
606 op = opcodes.OpInstanceStartup(instance_name=name,
607 force=opts.force,
608 ignore_offline_nodes=opts.ignore_offline,
609 no_remember=opts.no_remember,
610 startup_paused=opts.startup_paused)
611 # do not add these parameters to the opcode unless they're defined
612 if opts.hvparams:
613 op.hvparams = opts.hvparams
614 if opts.beparams:
615 op.beparams = opts.beparams
616 return op
617
618
619 def _RebootInstance(name, opts):
620 """Reboot instance(s).
621
622 This returns the opcode to reboot an instance, and its decorator
623 will wrap this into a loop rebooting all desired instances.
624
625 @param name: the name of the instance to act on
626 @param opts: the command line options selected by the user
627 @return: the opcode needed for the operation
628
629 """
630 return opcodes.OpInstanceReboot(instance_name=name,
631 reboot_type=opts.reboot_type,
632 ignore_secondaries=opts.ignore_secondaries,
633 shutdown_timeout=opts.shutdown_timeout)
634
635
636 def _ShutdownInstance(name, opts):
637 """Shutdown an instance.
638
639 This returns the opcode to shutdown an instance, and its decorator
640 will wrap this into a loop shutting down all desired instances.
641
642 @param name: the name of the instance to act on
643 @param opts: the command line options selected by the user
644 @return: the opcode needed for the operation
645
646 """
647 return opcodes.OpInstanceShutdown(instance_name=name,
648 force=opts.force,
649 timeout=opts.timeout,
650 ignore_offline_nodes=opts.ignore_offline,
651 no_remember=opts.no_remember)
652
653
654 def ReplaceDisks(opts, args):
655 """Replace the disks of an instance
656
657 @param opts: the command line options selected by the user
658 @type args: list
659 @param args: should contain only one element, the instance name
660 @rtype: int
661 @return: the desired exit code
662
663 """
664 new_2ndary = opts.dst_node
665 iallocator = opts.iallocator
666 if opts.disks is None:
667 disks = []
668 else:
669 try:
670 disks = [int(i) for i in opts.disks.split(",")]
671 except (TypeError, ValueError), err:
672 raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
673 errors.ECODE_INVAL)
674 cnt = [opts.on_primary, opts.on_secondary, opts.auto,
675 new_2ndary is not None, iallocator is not None].count(True)
676 if cnt != 1:
677 raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
678 " options must be passed", errors.ECODE_INVAL)
679 elif opts.on_primary:
680 mode = constants.REPLACE_DISK_PRI
681 elif opts.on_secondary:
682 mode = constants.REPLACE_DISK_SEC
683 elif opts.auto:
684 mode = constants.REPLACE_DISK_AUTO
685 if disks:
686 raise errors.OpPrereqError("Cannot specify disks when using automatic"
687 " mode", errors.ECODE_INVAL)
688 elif new_2ndary is not None or iallocator is not None:
689 # replace secondary
690 mode = constants.REPLACE_DISK_CHG
691
692 op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
693 remote_node=new_2ndary, mode=mode,
694 iallocator=iallocator,
695 early_release=opts.early_release,
696 ignore_ipolicy=opts.ignore_ipolicy)
697 SubmitOrSend(op, opts)
698 return 0
699
700
701 def FailoverInstance(opts, args):
702 """Failover an instance.
703
704 The failover is done by shutting it down on its present node and
705 starting it on the secondary.
706
707 @param opts: the command line options selected by the user
708 @type args: list
709 @param args: should contain only one element, the instance name
710 @rtype: int
711 @return: the desired exit code
712
713 """
714 cl = GetClient()
715 instance_name = args[0]
716 force = opts.force
717 iallocator = opts.iallocator
718 target_node = opts.dst_node
719
720 if iallocator and target_node:
721 raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
722 " node (-n) but not both", errors.ECODE_INVAL)
723
724 if not force:
725 _EnsureInstancesExist(cl, [instance_name])
726
727 usertext = ("Failover will happen to image %s."
728 " This requires a shutdown of the instance. Continue?" %
729 (instance_name,))
730 if not AskUser(usertext):
731 return 1
732
733 op = opcodes.OpInstanceFailover(instance_name=instance_name,
734 ignore_consistency=opts.ignore_consistency,
735 shutdown_timeout=opts.shutdown_timeout,
736 iallocator=iallocator,
737 target_node=target_node,
738 ignore_ipolicy=opts.ignore_ipolicy)
739 SubmitOrSend(op, opts, cl=cl)
740 return 0
741
742
743 def MigrateInstance(opts, args):
744 """Migrate an instance.
745
746 The migrate is done without shutdown.
747
748 @param opts: the command line options selected by the user
749 @type args: list
750 @param args: should contain only one element, the instance name
751 @rtype: int
752 @return: the desired exit code
753
754 """
755 cl = GetClient()
756 instance_name = args[0]
757 force = opts.force
758 iallocator = opts.iallocator
759 target_node = opts.dst_node
760
761 if iallocator and target_node:
762 raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
763 " node (-n) but not both", errors.ECODE_INVAL)
764
765 if not force:
766 _EnsureInstancesExist(cl, [instance_name])
767
768 if opts.cleanup:
769 usertext = ("Instance %s will be recovered from a failed migration."
770 " Note that the migration procedure (including cleanup)" %
771 (instance_name,))
772 else:
773 usertext = ("Instance %s will be migrated. Note that migration" %
774 (instance_name,))
775 usertext += (" might impact the instance if anything goes wrong"
776 " (e.g. due to bugs in the hypervisor). Continue?")
777 if not AskUser(usertext):
778 return 1
779
780 # this should be removed once --non-live is deprecated
781 if not opts.live and opts.migration_mode is not None:
782 raise errors.OpPrereqError("Only one of the --non-live and "
783 "--migration-mode options can be passed",
784 errors.ECODE_INVAL)
785 if not opts.live: # --non-live passed
786 mode = constants.HT_MIGRATION_NONLIVE
787 else:
788 mode = opts.migration_mode
789
790 op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
791 cleanup=opts.cleanup, iallocator=iallocator,
792 target_node=target_node,
793 allow_failover=opts.allow_failover,
794 allow_runtime_changes=opts.allow_runtime_chgs,
795 ignore_ipolicy=opts.ignore_ipolicy)
796 SubmitOrSend(op, cl=cl, opts=opts)
797 return 0
798
799
800 def MoveInstance(opts, args):
801 """Move an instance.
802
803 @param opts: the command line options selected by the user
804 @type args: list
805 @param args: should contain only one element, the instance name
806 @rtype: int
807 @return: the desired exit code
808
809 """
810 cl = GetClient()
811 instance_name = args[0]
812 force = opts.force
813
814 if not force:
815 usertext = ("Instance %s will be moved."
816 " This requires a shutdown of the instance. Continue?" %
817 (instance_name,))
818 if not AskUser(usertext):
819 return 1
820
821 op = opcodes.OpInstanceMove(instance_name=instance_name,
822 target_node=opts.node,
823 shutdown_timeout=opts.shutdown_timeout,
824 ignore_consistency=opts.ignore_consistency,
825 ignore_ipolicy=opts.ignore_ipolicy)
826 SubmitOrSend(op, opts, cl=cl)
827 return 0
828
829
830 def ConnectToInstanceConsole(opts, args):
831 """Connect to the console of an instance.
832
833 @param opts: the command line options selected by the user
834 @type args: list
835 @param args: should contain only one element, the instance name
836 @rtype: int
837 @return: the desired exit code
838
839 """
840 instance_name = args[0]
841
842 cl = GetClient()
843 try:
844 cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
845 ((console_data, oper_state), ) = \
846 cl.QueryInstances([instance_name], ["console", "oper_state"], False)
847 finally:
848 # Ensure client connection is closed while external commands are run
849 cl.Close()
850
851 del cl
852
853 if not console_data:
854 if oper_state:
855 # Instance is running
856 raise errors.OpExecError("Console information for instance %s is"
857 " unavailable" % instance_name)
858 else:
859 raise errors.OpExecError("Instance %s is not running, can't get console" %
860 instance_name)
861
862 return _DoConsole(objects.InstanceConsole.FromDict(console_data),
863 opts.show_command, cluster_name)
864
865
866 def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
867 _runcmd_fn=utils.RunCmd):
868 """Acts based on the result of L{opcodes.OpInstanceConsole}.
869
870 @type console: L{objects.InstanceConsole}
871 @param console: Console object
872 @type show_command: bool
873 @param show_command: Whether to just display commands
874 @type cluster_name: string
875 @param cluster_name: Cluster name as retrieved from master daemon
876
877 """
878 assert console.Validate()
879
880 if console.kind == constants.CONS_MESSAGE:
881 feedback_fn(console.message)
882 elif console.kind == constants.CONS_VNC:
883 feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
884 " URL <vnc://%s:%s/>",
885 console.instance, console.host, console.port,
886 console.display, console.host, console.port)
887 elif console.kind == constants.CONS_SPICE:
888 feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
889 console.host, console.port)
890 elif console.kind == constants.CONS_SSH:
891 # Convert to string if not already one
892 if isinstance(console.command, basestring):
893 cmd = console.command
894 else:
895 cmd = utils.ShellQuoteArgs(console.command)
896
897 srun = ssh.SshRunner(cluster_name=cluster_name)
898 ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
899 batch=True, quiet=False, tty=True)
900
901 if show_command:
902 feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
903 else:
904 result = _runcmd_fn(ssh_cmd, interactive=True)
905 if result.failed:
906 logging.error("Console command \"%s\" failed with reason '%s' and"
907 " output %r", result.cmd, result.fail_reason,
908 result.output)
909 raise errors.OpExecError("Connection to console of instance %s failed,"
910 " please check cluster configuration" %
911 console.instance)
912 else:
913 raise errors.GenericError("Unknown console type '%s'" % console.kind)
914
915 return constants.EXIT_SUCCESS
916
917
918 def _FormatDiskDetails(dev_type, dev, roman):
919 """Formats the logical_id of a disk.
920
921 """
922 if dev_type == constants.DT_DRBD8:
923 drbd_info = dev["drbd_info"]
924 data = [
925 ("nodeA", "%s, minor=%s" %
926 (drbd_info["primary_node"],
927 compat.TryToRoman(drbd_info["primary_minor"],
928 convert=roman))),
929 ("nodeB", "%s, minor=%s" %
930 (drbd_info["secondary_node"],
931 compat.TryToRoman(drbd_info["secondary_minor"],
932 convert=roman))),
933 ("port", str(compat.TryToRoman(drbd_info["port"], convert=roman))),
934 ("auth key", str(drbd_info["secret"])),
935 ]
936 elif dev_type == constants.DT_PLAIN:
937 vg_name, lv_name = dev["logical_id"]
938 data = ["%s/%s" % (vg_name, lv_name)]
939 else:
940 data = [str(dev["logical_id"])]
941
942 return data
943
944
945 def _FormatListInfo(data):
946 return list(str(i) for i in data)
947
948
949 def _FormatBlockDevInfo(idx, top_level, dev, roman):
950 """Show block device information.
951
952 This is only used by L{ShowInstanceConfig}, but it's too big to be
953 left for an inline definition.
954
955 @type idx: int
956 @param idx: the index of the current disk
957 @type top_level: boolean
958 @param top_level: if this a top-level disk?
959 @type dev: dict
960 @param dev: dictionary with disk information
961 @type roman: boolean
962 @param roman: whether to try to use roman integers
963 @return: a list of either strings, tuples or lists
964 (which should be formatted at a higher indent level)
965
966 """
967 def helper(dtype, status):
968 """Format one line for physical device status.
969
970 @type dtype: str
971 @param dtype: a constant from the L{constants.DTS_BLOCK} set
972 @type status: tuple
973 @param status: a tuple as returned from L{backend.FindBlockDevice}
974 @return: the string representing the status
975
976 """
977 if not status:
978 return "not active"
979 txt = ""
980 (path, major, minor, syncp, estt, degr, ldisk_status) = status
981 if major is None:
982 major_string = "N/A"
983 else:
984 major_string = str(compat.TryToRoman(major, convert=roman))
985
986 if minor is None:
987 minor_string = "N/A"
988 else:
989 minor_string = str(compat.TryToRoman(minor, convert=roman))
990
991 txt += ("%s (%s:%s)" % (path, major_string, minor_string))
992 if dtype in (constants.DT_DRBD8, ):
993 if syncp is not None:
994 sync_text = "*RECOVERING* %5.2f%%," % syncp
995 if estt:
996 sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
997 else:
998 sync_text += " ETA unknown"
999 else:
1000 sync_text = "in sync"
1001 if degr:
1002 degr_text = "*DEGRADED*"
1003 else:
1004 degr_text = "ok"
1005 if ldisk_status == constants.LDS_FAULTY:
1006 ldisk_text = " *MISSING DISK*"
1007 elif ldisk_status == constants.LDS_UNKNOWN:
1008 ldisk_text = " *UNCERTAIN STATE*"
1009 else:
1010 ldisk_text = ""
1011 txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1012 elif dtype == constants.DT_PLAIN:
1013 if ldisk_status == constants.LDS_FAULTY:
1014 ldisk_text = " *FAILED* (failed drive?)"
1015 else:
1016 ldisk_text = ""
1017 txt += ldisk_text
1018 return txt
1019
1020 # the header
1021 if top_level:
1022 if dev["iv_name"] is not None:
1023 txt = dev["iv_name"]
1024 else:
1025 txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1026 else:
1027 txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1028 if isinstance(dev["size"], int):
1029 nice_size = utils.FormatUnit(dev["size"], "h")
1030 else:
1031 nice_size = str(dev["size"])
1032 data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1033 if top_level:
1034 if dev["spindles"] is not None:
1035 data.append(("spindles", dev["spindles"]))
1036 data.append(("access mode", dev["mode"]))
1037 if dev["logical_id"] is not None:
1038 try:
1039 l_id = _FormatDiskDetails(dev["dev_type"], dev, roman)
1040 except ValueError:
1041 l_id = [str(dev["logical_id"])]
1042 if len(l_id) == 1:
1043 data.append(("logical_id", l_id[0]))
1044 else:
1045 data.extend(l_id)
1046 elif dev["physical_id"] is not None:
1047 data.append(("physical_id:", _FormatListInfo(dev["physical_id"])))
1048
1049 if dev["pstatus"]:
1050 data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1051
1052 if dev["sstatus"]:
1053 data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1054
1055 data.append(("name", dev["name"]))
1056 data.append(("UUID", dev["uuid"]))
1057
1058 if dev["children"]:
1059 data.append(("child devices", [
1060 _FormatBlockDevInfo(c_idx, False, child, roman)
1061 for c_idx, child in enumerate(dev["children"])
1062 ]))
1063 return data
1064
1065
1066 def _FormatInstanceNicInfo(idx, nic):
1067 """Helper function for L{_FormatInstanceInfo()}"""
1068 (name, uuid, ip, mac, mode, link, _, netinfo) = nic
1069 network_name = None
1070 if netinfo:
1071 network_name = netinfo["name"]
1072 return [
1073 ("nic/%d" % idx, ""),
1074 ("MAC", str(mac)),
1075 ("IP", str(ip)),
1076 ("mode", str(mode)),
1077 ("link", str(link)),
1078 ("network", str(network_name)),
1079 ("UUID", str(uuid)),
1080 ("name", str(name)),
1081 ]
1082
1083
1084 def _FormatInstanceNodesInfo(instance):
1085 """Helper function for L{_FormatInstanceInfo()}"""
1086 pgroup = ("%s (UUID %s)" %
1087 (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1088 secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1089 (name, group_name, group_uuid))
1090 for (name, group_name, group_uuid) in
1091 zip(instance["snodes"],
1092 instance["snodes_group_names"],
1093 instance["snodes_group_uuids"]))
1094 return [
1095 [
1096 ("primary", instance["pnode"]),
1097 ("group", pgroup),
1098 ],
1099 [("secondaries", secs)],
1100 ]
1101
1102
1103 def _GetVncConsoleInfo(instance):
1104 """Helper function for L{_FormatInstanceInfo()}"""
1105 vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1106 None)
1107 if vnc_bind_address:
1108 port = instance["network_port"]
1109 display = int(port) - constants.VNC_BASE_PORT
1110 if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1111 vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1112 port,
1113 display)
1114 elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1115 vnc_console_port = ("%s:%s (node %s) (display %s)" %
1116 (vnc_bind_address, port,
1117 instance["pnode"], display))
1118 else:
1119 # vnc bind address is a file
1120 vnc_console_port = "%s:%s" % (instance["pnode"],
1121 vnc_bind_address)
1122 ret = "vnc to %s" % vnc_console_port
1123 else:
1124 ret = None
1125 return ret
1126
1127
1128 def _FormatInstanceInfo(instance, roman_integers):
1129 """Format instance information for L{cli.PrintGenericInfo()}"""
1130 istate = "configured to be %s" % instance["config_state"]
1131 if instance["run_state"]:
1132 istate += ", actual state is %s" % instance["run_state"]
1133 info = [
1134 ("Instance name", instance["name"]),
1135 ("UUID", instance["uuid"]),
1136 ("Serial number",
1137 str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1138 ("Creation time", utils.FormatTime(instance["ctime"])),
1139 ("Modification time", utils.FormatTime(instance["mtime"])),
1140 ("State", istate),
1141 ("Nodes", _FormatInstanceNodesInfo(instance)),
1142 ("Operating system", instance["os"]),
1143 ("Operating system parameters",
1144 FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1145 ]
1146
1147 if "network_port" in instance:
1148 info.append(("Allocated network port",
1149 str(compat.TryToRoman(instance["network_port"],
1150 convert=roman_integers))))
1151 info.append(("Hypervisor", instance["hypervisor"]))
1152 console = _GetVncConsoleInfo(instance)
1153 if console:
1154 info.append(("console connection", console))
1155 # deprecated "memory" value, kept for one version for compatibility
1156 # TODO(ganeti 2.7) remove.
1157 be_actual = copy.deepcopy(instance["be_actual"])
1158 be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1159 info.extend([
1160 ("Hypervisor parameters",
1161 FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1162 ("Back-end parameters",
1163 FormatParamsDictInfo(instance["be_instance"], be_actual)),
1164 ("NICs", [
1165 _FormatInstanceNicInfo(idx, nic)
1166 for (idx, nic) in enumerate(instance["nics"])
1167 ]),
1168 ("Disk template", instance["disk_template"]),
1169 ("Disks", [
1170 _FormatBlockDevInfo(idx, True, device, roman_integers)
1171 for (idx, device) in enumerate(instance["disks"])
1172 ]),
1173 ])
1174 return info
1175
1176
1177 def ShowInstanceConfig(opts, args):
1178 """Compute instance run-time status.
1179
1180 @param opts: the command line options selected by the user
1181 @type args: list
1182 @param args: either an empty list, and then we query all
1183 instances, or should contain a list of instance names
1184 @rtype: int
1185 @return: the desired exit code
1186
1187 """
1188 if not args and not opts.show_all:
1189 ToStderr("No instance selected."
1190 " Please pass in --all if you want to query all instances.\n"
1191 "Note that this can take a long time on a big cluster.")
1192 return 1
1193 elif args and opts.show_all:
1194 ToStderr("Cannot use --all if you specify instance names.")
1195 return 1
1196
1197 retcode = 0
1198 op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1199 use_locking=not opts.static)
1200 result = SubmitOpCode(op, opts=opts)
1201 if not result:
1202 ToStdout("No instances.")
1203 return 1
1204
1205 PrintGenericInfo([
1206 _FormatInstanceInfo(instance, opts.roman_integers)
1207 for instance in result.values()
1208 ])
1209 return retcode
1210
1211
1212 def _ConvertNicDiskModifications(mods):
1213 """Converts NIC/disk modifications from CLI to opcode.
1214
1215 When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
1216 disks at arbitrary indices, its parameter format changed. This function
1217 converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
1218 newer format and adds support for new-style requests (e.g. "--new 4:add").
1219
1220 @type mods: list of tuples
1221 @param mods: Modifications as given by command line parser
1222 @rtype: list of tuples
1223 @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1224
1225 """
1226 result = []
1227
1228 for (identifier, params) in mods:
1229 if identifier == constants.DDM_ADD:
1230 # Add item as last item (legacy interface)
1231 action = constants.DDM_ADD
1232 identifier = -1
1233 elif identifier == constants.DDM_REMOVE:
1234 # Remove last item (legacy interface)
1235 action = constants.DDM_REMOVE
1236 identifier = -1
1237 else:
1238 # Modifications and adding/removing at arbitrary indices
1239 add = params.pop(constants.DDM_ADD, _MISSING)
1240 remove = params.pop(constants.DDM_REMOVE, _MISSING)
1241 modify = params.pop(constants.DDM_MODIFY, _MISSING)
1242
1243 if modify is _MISSING:
1244 if not (add is _MISSING or remove is _MISSING):
1245 raise errors.OpPrereqError("Cannot add and remove at the same time",
1246 errors.ECODE_INVAL)
1247 elif add is not _MISSING:
1248 action = constants.DDM_ADD
1249 elif remove is not _MISSING:
1250 action = constants.DDM_REMOVE
1251 else:
1252 action = constants.DDM_MODIFY
1253
1254 elif add is _MISSING and remove is _MISSING:
1255 action = constants.DDM_MODIFY
1256 else:
1257 raise errors.OpPrereqError("Cannot modify and add/remove at the"
1258 " same time", errors.ECODE_INVAL)
1259
1260 assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1261
1262 if action == constants.DDM_REMOVE and params:
1263 raise errors.OpPrereqError("Not accepting parameters on removal",
1264 errors.ECODE_INVAL)
1265
1266 result.append((action, identifier, params))
1267
1268 return result
1269
1270
1271 def _ParseDiskSizes(mods):
1272 """Parses disk sizes in parameters.
1273
1274 """
1275 for (action, _, params) in mods:
1276 if params and constants.IDISK_SIZE in params:
1277 params[constants.IDISK_SIZE] = \
1278 utils.ParseUnit(params[constants.IDISK_SIZE])
1279 elif action == constants.DDM_ADD:
1280 raise errors.OpPrereqError("Missing required parameter 'size'",
1281 errors.ECODE_INVAL)
1282
1283 return mods
1284
1285
1286 def SetInstanceParams(opts, args):
1287 """Modifies an instance.
1288
1289 All parameters take effect only at the next restart of the instance.
1290
1291 @param opts: the command line options selected by the user
1292 @type args: list
1293 @param args: should contain only one element, the instance name
1294 @rtype: int
1295 @return: the desired exit code
1296
1297 """
1298 if not (opts.nics or opts.disks or opts.disk_template or
1299 opts.hvparams or opts.beparams or opts.os or opts.osparams or
1300 opts.offline_inst or opts.online_inst or opts.runtime_mem or
1301 opts.new_primary_node):
1302 ToStderr("Please give at least one of the parameters.")
1303 return 1
1304
1305 for param in opts.beparams:
1306 if isinstance(opts.beparams[param], basestring):
1307 if opts.beparams[param].lower() == "default":
1308 opts.beparams[param] = constants.VALUE_DEFAULT
1309
1310 utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1311 allowed_values=[constants.VALUE_DEFAULT])
1312
1313 for param in opts.hvparams:
1314 if isinstance(opts.hvparams[param], basestring):
1315 if opts.hvparams[param].lower() == "default":
1316 opts.hvparams[param] = constants.VALUE_DEFAULT
1317
1318 utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1319 allowed_values=[constants.VALUE_DEFAULT])
1320 FixHvParams(opts.hvparams)
1321
1322 nics = _ConvertNicDiskModifications(opts.nics)
1323 disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1324
1325 if (opts.disk_template and
1326 opts.disk_template in constants.DTS_INT_MIRROR and
1327 not opts.node):
1328 ToStderr("Changing the disk template to a mirrored one requires"
1329 " specifying a secondary node")
1330 return 1
1331
1332 if opts.offline_inst:
1333 offline = True
1334 elif opts.online_inst:
1335 offline = False
1336 else:
1337 offline = None
1338
1339 op = opcodes.OpInstanceSetParams(instance_name=args[0],
1340 nics=nics,
1341 disks=disks,
1342 disk_template=opts.disk_template,
1343 remote_node=opts.node,
1344 pnode=opts.new_primary_node,
1345 hvparams=opts.hvparams,
1346 beparams=opts.beparams,
1347 runtime_mem=opts.runtime_mem,
1348 os_name=opts.os,
1349 osparams=opts.osparams,
1350 force_variant=opts.force_variant,
1351 force=opts.force,
1352 wait_for_sync=opts.wait_for_sync,
1353 offline=offline,
1354 conflicts_check=opts.conflicts_check,
1355 ignore_ipolicy=opts.ignore_ipolicy)
1356
1357 # even if here we process the result, we allow submit only
1358 result = SubmitOrSend(op, opts)
1359
1360 if result:
1361 ToStdout("Modified instance %s", args[0])
1362 for param, data in result:
1363 ToStdout(" - %-5s -> %s", param, data)
1364 ToStdout("Please don't forget that most parameters take effect"
1365 " only at the next (re)start of the instance initiated by"
1366 " ganeti; restarting from within the instance will"
1367 " not be enough.")
1368 return 0
1369
1370
1371 def ChangeGroup(opts, args):
1372 """Moves an instance to another group.
1373
1374 """
1375 (instance_name, ) = args
1376
1377 cl = GetClient()
1378
1379 op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1380 iallocator=opts.iallocator,
1381 target_groups=opts.to,
1382 early_release=opts.early_release)
1383 result = SubmitOrSend(op, opts, cl=cl)
1384
1385 # Keep track of submitted jobs
1386 jex = JobExecutor(cl=cl, opts=opts)
1387
1388 for (status, job_id) in result[constants.JOB_IDS_KEY]:
1389 jex.AddJobId(None, status, job_id)
1390
1391 results = jex.GetResults()
1392 bad_cnt = len([row for row in results if not row[0]])
1393 if bad_cnt == 0:
1394 ToStdout("Instance '%s' changed group successfully.", instance_name)
1395 rcode = constants.EXIT_SUCCESS
1396 else:
1397 ToStdout("There were %s errors while changing group of instance '%s'.",
1398 bad_cnt, instance_name)
1399 rcode = constants.EXIT_FAILURE
1400
1401 return rcode
1402
1403
1404 # multi-instance selection options
1405 m_force_multi = cli_option("--force-multiple", dest="force_multi",
1406 help="Do not ask for confirmation when more than"
1407 " one instance is affected",
1408 action="store_true", default=False)
1409
1410 m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1411 help="Filter by nodes (primary only)",
1412 const=_EXPAND_NODES_PRI, action="store_const")
1413
1414 m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1415 help="Filter by nodes (secondary only)",
1416 const=_EXPAND_NODES_SEC, action="store_const")
1417
1418 m_node_opt = cli_option("--node", dest="multi_mode",
1419 help="Filter by nodes (primary and secondary)",
1420 const=_EXPAND_NODES_BOTH, action="store_const")
1421
1422 m_clust_opt = cli_option("--all", dest="multi_mode",
1423 help="Select all instances in the cluster",
1424 const=_EXPAND_CLUSTER, action="store_const")
1425
1426 m_inst_opt = cli_option("--instance", dest="multi_mode",
1427 help="Filter by instance name [default]",
1428 const=_EXPAND_INSTANCES, action="store_const")
1429
1430 m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1431 help="Filter by node tag",
1432 const=_EXPAND_NODES_BOTH_BY_TAGS,
1433 action="store_const")
1434
1435 m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1436 help="Filter by primary node tag",
1437 const=_EXPAND_NODES_PRI_BY_TAGS,
1438 action="store_const")
1439
1440 m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1441 help="Filter by secondary node tag",
1442 const=_EXPAND_NODES_SEC_BY_TAGS,
1443 action="store_const")
1444
1445 m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1446 help="Filter by instance tag",
1447 const=_EXPAND_INSTANCES_BY_TAGS,
1448 action="store_const")
1449
1450 # this is defined separately due to readability only
1451 add_opts = [
1452 NOSTART_OPT,
1453 OS_OPT,
1454 FORCE_VARIANT_OPT,
1455 NO_INSTALL_OPT,
1456 IGNORE_IPOLICY_OPT,
1457 ]
1458
1459 commands = {
1460 "add": (
1461 AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
1462 "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1463 "Creates and adds a new instance to the cluster"),
1464 "batch-create": (
1465 BatchCreate, [ArgFile(min=1, max=1)],
1466 [DRY_RUN_OPT, PRIORITY_OPT, IALLOCATOR_OPT] + SUBMIT_OPTS,
1467 "<instances.json>",
1468 "Create a bunch of instances based on specs in the file."),
1469 "console": (
1470 ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1471 [SHOWCMD_OPT, PRIORITY_OPT],
1472 "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1473 "failover": (
1474 FailoverInstance, ARGS_ONE_INSTANCE,
1475 [FORCE_OPT, IGNORE_CONSIST_OPT] + SUBMIT_OPTS +
1476 [SHUTDOWN_TIMEOUT_OPT,
1477 DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1478 IGNORE_IPOLICY_OPT, CLEANUP_OPT],
1479 "[-f] <instance>", "Stops the instance, changes its primary node and"
1480 " (if it was originally running) starts it on the new node"
1481 " (the secondary for mirrored instances or any node"
1482 " for shared storage)."),
1483 "migrate": (
1484 MigrateInstance, ARGS_ONE_INSTANCE,
1485 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1486 PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1487 IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
1488 "[-f] <instance>", "Migrate instance to its secondary node"
1489 " (only for mirrored instances)"),
1490 "move": (
1491 MoveInstance, ARGS_ONE_INSTANCE,
1492 [FORCE_OPT] + SUBMIT_OPTS +
1493 [SINGLE_NODE_OPT,
1494 SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT,
1495 IGNORE_IPOLICY_OPT],
1496 "[-f] <instance>", "Move instance to an arbitrary node"
1497 " (only for instances of type file and lv)"),
1498 "info": (
1499 ShowInstanceConfig, ARGS_MANY_INSTANCES,
1500 [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1501 "[-s] {--all | <instance>...}",
1502 "Show information on the specified instance(s)"),
1503 "list": (
1504 ListInstances, ARGS_MANY_INSTANCES,
1505 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1506 FORCE_FILTER_OPT],
1507 "[<instance>...]",
1508 "Lists the instances and their status. The available fields can be shown"
1509 " using the \"list-fields\" command (see the man page for details)."
1510 " The default field list is (in order): %s." %
1511 utils.CommaJoin(_LIST_DEF_FIELDS),
1512 ),
1513 "list-fields": (
1514 ListInstanceFields, [ArgUnknown()],
1515 [NOHDR_OPT, SEP_OPT],
1516 "[fields...]",
1517 "Lists all available fields for instances"),
1518 "reinstall": (
1519 ReinstallInstance, [ArgInstance()],
1520 [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1521 m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1522 m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT]
1523 + SUBMIT_OPTS + [DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1524 "[-f] <instance>", "Reinstall a stopped instance"),
1525 "remove": (
1526 RemoveInstance, ARGS_ONE_INSTANCE,
1527 [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT] + SUBMIT_OPTS
1528 + [DRY_RUN_OPT, PRIORITY_OPT],
1529 "[-f] <instance>", "Shuts down the instance and removes it"),
1530 "rename": (
1531 RenameInstance,
1532 [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1533 [NOIPCHECK_OPT, NONAMECHECK_OPT] + SUBMIT_OPTS
1534 + [DRY_RUN_OPT, PRIORITY_OPT],
1535 "<instance> <new_name>", "Rename the instance"),
1536 "replace-disks": (
1537 ReplaceDisks, ARGS_ONE_INSTANCE,
1538 [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1539 NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT] + SUBMIT_OPTS
1540 + [DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1541 "[-s|-p|-a|-n NODE|-I NAME] <instance>",
1542 "Replaces disks for the instance"),
1543 "modify": (
1544 SetInstanceParams, ARGS_ONE_INSTANCE,
1545 [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT] + SUBMIT_OPTS +
1546 [DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1547 OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
1548 ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
1549 NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT],
1550 "<instance>", "Alters the parameters of an instance"),
1551 "shutdown": (
1552 GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1553 [FORCE_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1554 m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1555 m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT] + SUBMIT_OPTS
1556 + [DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1557 "<instance>", "Stops an instance"),
1558 "startup": (
1559 GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1560 [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1561 m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1562 m_inst_tags_opt, m_clust_opt, m_inst_opt] + SUBMIT_OPTS +
1563 [HVOPTS_OPT,
1564 BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1565 NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1566 "<instance>", "Starts an instance"),
1567 "reboot": (
1568 GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1569 [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1570 m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt] + SUBMIT_OPTS +
1571 [m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1572 m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1573 "<instance>", "Reboots an instance"),
1574 "activate-disks": (
1575 ActivateDisks, ARGS_ONE_INSTANCE,
1576 SUBMIT_OPTS + [IGNORE_SIZE_OPT, PRIORITY_OPT, WFSYNC_OPT],
1577 "<instance>", "Activate an instance's disks"),
1578 "deactivate-disks": (
1579 DeactivateDisks, ARGS_ONE_INSTANCE,
1580 [FORCE_OPT] + SUBMIT_OPTS + [DRY_RUN_OPT, PRIORITY_OPT],
1581 "[-f] <instance>", "Deactivate an instance's disks"),
1582 "recreate-disks": (
1583 RecreateDisks, ARGS_ONE_INSTANCE,
1584 SUBMIT_OPTS +
1585 [DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
1586 IALLOCATOR_OPT],
1587 "<instance>", "Recreate an instance's disks"),
1588 "grow-disk": (
1589 GrowDisk,
1590 [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1591 ArgUnknown(min=1, max=1)],
1592 SUBMIT_OPTS + [NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
1593 "<instance> <disk> <size>", "Grow an instance's disk"),
1594 "change-group": (
1595 ChangeGroup, ARGS_ONE_INSTANCE,
1596 [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT]
1597 + SUBMIT_OPTS,
1598 "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1599 "list-tags": (
1600 ListTags, ARGS_ONE_INSTANCE, [],
1601 "<instance_name>", "List the tags of the given instance"),
1602 "add-tags": (
1603 AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1604 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1605 "<instance_name> tag...", "Add tags to the given instance"),
1606 "remove-tags": (
1607 RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1608 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1609 "<instance_name> tag...", "Remove tags from given instance"),
1610 }
1611
1612 #: dictionary with aliases for commands
1613 aliases = {
1614 "start": "startup",
1615 "stop": "shutdown",
1616 "show": "info",
1617 }
1618
1619
1620 def Main():
1621 return GenericMain(commands, aliases=aliases,
1622 override={"tag_type": constants.TAG_INSTANCE},
1623 env_override=_ENV_OVERRIDE)