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