07db3a62cc0ac728f5dd137f03de2ab53974be7a
[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
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 _FormatLogicalID(dev_type, logical_id, roman):
919   """Formats the logical_id of a disk.
920
921   """
922   if dev_type == constants.LD_DRBD8:
923     node_a, node_b, port, minor_a, minor_b, key = logical_id
924     data = [
925       ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
926                                                             convert=roman))),
927       ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
928                                                             convert=roman))),
929       ("port", str(compat.TryToRoman(port, convert=roman))),
930       ("auth key", str(key)),
931       ]
932   elif dev_type == constants.LD_LV:
933     vg_name, lv_name = logical_id
934     data = ["%s/%s" % (vg_name, lv_name)]
935   else:
936     data = [str(logical_id)]
937
938   return data
939
940
941 def _FormatListInfo(data):
942   return list(str(i) for i in data)
943
944
945 def _FormatBlockDevInfo(idx, top_level, dev, roman):
946   """Show block device information.
947
948   This is only used by L{ShowInstanceConfig}, but it's too big to be
949   left for an inline definition.
950
951   @type idx: int
952   @param idx: the index of the current disk
953   @type top_level: boolean
954   @param top_level: if this a top-level disk?
955   @type dev: dict
956   @param dev: dictionary with disk information
957   @type roman: boolean
958   @param roman: whether to try to use roman integers
959   @return: a list of either strings, tuples or lists
960       (which should be formatted at a higher indent level)
961
962   """
963   def helper(dtype, status):
964     """Format one line for physical device status.
965
966     @type dtype: str
967     @param dtype: a constant from the L{constants.LDS_BLOCK} set
968     @type status: tuple
969     @param status: a tuple as returned from L{backend.FindBlockDevice}
970     @return: the string representing the status
971
972     """
973     if not status:
974       return "not active"
975     txt = ""
976     (path, major, minor, syncp, estt, degr, ldisk_status) = status
977     if major is None:
978       major_string = "N/A"
979     else:
980       major_string = str(compat.TryToRoman(major, convert=roman))
981
982     if minor is None:
983       minor_string = "N/A"
984     else:
985       minor_string = str(compat.TryToRoman(minor, convert=roman))
986
987     txt += ("%s (%s:%s)" % (path, major_string, minor_string))
988     if dtype in (constants.LD_DRBD8, ):
989       if syncp is not None:
990         sync_text = "*RECOVERING* %5.2f%%," % syncp
991         if estt:
992           sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
993         else:
994           sync_text += " ETA unknown"
995       else:
996         sync_text = "in sync"
997       if degr:
998         degr_text = "*DEGRADED*"
999       else:
1000         degr_text = "ok"
1001       if ldisk_status == constants.LDS_FAULTY:
1002         ldisk_text = " *MISSING DISK*"
1003       elif ldisk_status == constants.LDS_UNKNOWN:
1004         ldisk_text = " *UNCERTAIN STATE*"
1005       else:
1006         ldisk_text = ""
1007       txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1008     elif dtype == constants.LD_LV:
1009       if ldisk_status == constants.LDS_FAULTY:
1010         ldisk_text = " *FAILED* (failed drive?)"
1011       else:
1012         ldisk_text = ""
1013       txt += ldisk_text
1014     return txt
1015
1016   # the header
1017   if top_level:
1018     if dev["iv_name"] is not None:
1019       txt = dev["iv_name"]
1020     else:
1021       txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1022   else:
1023     txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1024   if isinstance(dev["size"], int):
1025     nice_size = utils.FormatUnit(dev["size"], "h")
1026   else:
1027     nice_size = str(dev["size"])
1028   data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1029   if top_level:
1030     data.append(("access mode", dev["mode"]))
1031   if dev["logical_id"] is not None:
1032     try:
1033       l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1034     except ValueError:
1035       l_id = [str(dev["logical_id"])]
1036     if len(l_id) == 1:
1037       data.append(("logical_id", l_id[0]))
1038     else:
1039       data.extend(l_id)
1040   elif dev["physical_id"] is not None:
1041     data.append(("physical_id:", _FormatListInfo(dev["physical_id"])))
1042
1043   if dev["pstatus"]:
1044     data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1045
1046   if dev["sstatus"]:
1047     data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1048
1049   data.append(("name", dev["name"]))
1050   data.append(("UUID", dev["uuid"]))
1051
1052   if dev["children"]:
1053     data.append(("child devices", [
1054       _FormatBlockDevInfo(c_idx, False, child, roman)
1055       for c_idx, child in enumerate(dev["children"])
1056       ]))
1057   return data
1058
1059
1060 def _FormatInstanceNicInfo(idx, nic):
1061   """Helper function for L{_FormatInstanceInfo()}"""
1062   (name, uuid, ip, mac, mode, link, _, netinfo) = nic
1063   network_name = None
1064   if netinfo:
1065     network_name = netinfo["name"]
1066   return [
1067     ("nic/%d" % idx, ""),
1068     ("MAC", str(mac)),
1069     ("IP", str(ip)),
1070     ("mode", str(mode)),
1071     ("link", str(link)),
1072     ("network", str(network_name)),
1073     ("UUID", str(uuid)),
1074     ("name", str(name)),
1075     ]
1076
1077
1078 def _FormatInstanceNodesInfo(instance):
1079   """Helper function for L{_FormatInstanceInfo()}"""
1080   pgroup = ("%s (UUID %s)" %
1081             (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1082   secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1083                           (name, group_name, group_uuid))
1084                          for (name, group_name, group_uuid) in
1085                            zip(instance["snodes"],
1086                                instance["snodes_group_names"],
1087                                instance["snodes_group_uuids"]))
1088   return [
1089     [
1090       ("primary", instance["pnode"]),
1091       ("group", pgroup),
1092       ],
1093     [("secondaries", secs)],
1094     ]
1095
1096
1097 def _GetVncConsoleInfo(instance):
1098   """Helper function for L{_FormatInstanceInfo()}"""
1099   vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1100                                                None)
1101   if vnc_bind_address:
1102     port = instance["network_port"]
1103     display = int(port) - constants.VNC_BASE_PORT
1104     if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1105       vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1106                                                  port,
1107                                                  display)
1108     elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1109       vnc_console_port = ("%s:%s (node %s) (display %s)" %
1110                            (vnc_bind_address, port,
1111                             instance["pnode"], display))
1112     else:
1113       # vnc bind address is a file
1114       vnc_console_port = "%s:%s" % (instance["pnode"],
1115                                     vnc_bind_address)
1116     ret = "vnc to %s" % vnc_console_port
1117   else:
1118     ret = None
1119   return ret
1120
1121
1122 def _FormatInstanceInfo(instance, roman_integers):
1123   """Format instance information for L{cli.PrintGenericInfo()}"""
1124   istate = "configured to be %s" % instance["config_state"]
1125   if instance["run_state"]:
1126     istate += ", actual state is %s" % instance["run_state"]
1127   info = [
1128     ("Instance name", instance["name"]),
1129     ("UUID", instance["uuid"]),
1130     ("Serial number",
1131      str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1132     ("Creation time", utils.FormatTime(instance["ctime"])),
1133     ("Modification time", utils.FormatTime(instance["mtime"])),
1134     ("State", istate),
1135     ("Nodes", _FormatInstanceNodesInfo(instance)),
1136     ("Operating system", instance["os"]),
1137     ("Operating system parameters",
1138      FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1139     ]
1140
1141   if "network_port" in instance:
1142     info.append(("Allocated network port",
1143                  str(compat.TryToRoman(instance["network_port"],
1144                                        convert=roman_integers))))
1145   info.append(("Hypervisor", instance["hypervisor"]))
1146   console = _GetVncConsoleInfo(instance)
1147   if console:
1148     info.append(("console connection", console))
1149   # deprecated "memory" value, kept for one version for compatibility
1150   # TODO(ganeti 2.7) remove.
1151   be_actual = copy.deepcopy(instance["be_actual"])
1152   be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1153   info.extend([
1154     ("Hypervisor parameters",
1155      FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1156     ("Back-end parameters",
1157      FormatParamsDictInfo(instance["be_instance"], be_actual)),
1158     ("NICs", [
1159       _FormatInstanceNicInfo(idx, nic)
1160       for (idx, nic) in enumerate(instance["nics"])
1161       ]),
1162     ("Disk template", instance["disk_template"]),
1163     ("Disks", [
1164       _FormatBlockDevInfo(idx, True, device, roman_integers)
1165       for (idx, device) in enumerate(instance["disks"])
1166       ]),
1167     ])
1168   return info
1169
1170
1171 def ShowInstanceConfig(opts, args):
1172   """Compute instance run-time status.
1173
1174   @param opts: the command line options selected by the user
1175   @type args: list
1176   @param args: either an empty list, and then we query all
1177       instances, or should contain a list of instance names
1178   @rtype: int
1179   @return: the desired exit code
1180
1181   """
1182   if not args and not opts.show_all:
1183     ToStderr("No instance selected."
1184              " Please pass in --all if you want to query all instances.\n"
1185              "Note that this can take a long time on a big cluster.")
1186     return 1
1187   elif args and opts.show_all:
1188     ToStderr("Cannot use --all if you specify instance names.")
1189     return 1
1190
1191   retcode = 0
1192   op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1193                                    use_locking=not opts.static)
1194   result = SubmitOpCode(op, opts=opts)
1195   if not result:
1196     ToStdout("No instances.")
1197     return 1
1198
1199   PrintGenericInfo([
1200     _FormatInstanceInfo(instance, opts.roman_integers)
1201     for instance in result.values()
1202     ])
1203   return retcode
1204
1205
1206 def _ConvertNicDiskModifications(mods):
1207   """Converts NIC/disk modifications from CLI to opcode.
1208
1209   When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
1210   disks at arbitrary indices, its parameter format changed. This function
1211   converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
1212   newer format and adds support for new-style requests (e.g. "--new 4:add").
1213
1214   @type mods: list of tuples
1215   @param mods: Modifications as given by command line parser
1216   @rtype: list of tuples
1217   @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1218
1219   """
1220   result = []
1221
1222   for (identifier, params) in mods:
1223     if identifier == constants.DDM_ADD:
1224       # Add item as last item (legacy interface)
1225       action = constants.DDM_ADD
1226       identifier = -1
1227     elif identifier == constants.DDM_REMOVE:
1228       # Remove last item (legacy interface)
1229       action = constants.DDM_REMOVE
1230       identifier = -1
1231     else:
1232       # Modifications and adding/removing at arbitrary indices
1233       add = params.pop(constants.DDM_ADD, _MISSING)
1234       remove = params.pop(constants.DDM_REMOVE, _MISSING)
1235       modify = params.pop(constants.DDM_MODIFY, _MISSING)
1236
1237       if modify is _MISSING:
1238         if not (add is _MISSING or remove is _MISSING):
1239           raise errors.OpPrereqError("Cannot add and remove at the same time",
1240                                      errors.ECODE_INVAL)
1241         elif add is not _MISSING:
1242           action = constants.DDM_ADD
1243         elif remove is not _MISSING:
1244           action = constants.DDM_REMOVE
1245         else:
1246           action = constants.DDM_MODIFY
1247
1248       elif add is _MISSING and remove is _MISSING:
1249         action = constants.DDM_MODIFY
1250       else:
1251         raise errors.OpPrereqError("Cannot modify and add/remove at the"
1252                                    " same time", errors.ECODE_INVAL)
1253
1254       assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1255
1256     if action == constants.DDM_REMOVE and params:
1257       raise errors.OpPrereqError("Not accepting parameters on removal",
1258                                  errors.ECODE_INVAL)
1259
1260     result.append((action, identifier, params))
1261
1262   return result
1263
1264
1265 def _ParseDiskSizes(mods):
1266   """Parses disk sizes in parameters.
1267
1268   """
1269   for (action, _, params) in mods:
1270     if params and constants.IDISK_SIZE in params:
1271       params[constants.IDISK_SIZE] = \
1272         utils.ParseUnit(params[constants.IDISK_SIZE])
1273     elif action == constants.DDM_ADD:
1274       raise errors.OpPrereqError("Missing required parameter 'size'",
1275                                  errors.ECODE_INVAL)
1276
1277   return mods
1278
1279
1280 def SetInstanceParams(opts, args):
1281   """Modifies an instance.
1282
1283   All parameters take effect only at the next restart of the instance.
1284
1285   @param opts: the command line options selected by the user
1286   @type args: list
1287   @param args: should contain only one element, the instance name
1288   @rtype: int
1289   @return: the desired exit code
1290
1291   """
1292   if not (opts.nics or opts.disks or opts.disk_template or
1293           opts.hvparams or opts.beparams or opts.os or opts.osparams or
1294           opts.offline_inst or opts.online_inst or opts.runtime_mem or
1295           opts.new_primary_node):
1296     ToStderr("Please give at least one of the parameters.")
1297     return 1
1298
1299   for param in opts.beparams:
1300     if isinstance(opts.beparams[param], basestring):
1301       if opts.beparams[param].lower() == "default":
1302         opts.beparams[param] = constants.VALUE_DEFAULT
1303
1304   utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1305                       allowed_values=[constants.VALUE_DEFAULT])
1306
1307   for param in opts.hvparams:
1308     if isinstance(opts.hvparams[param], basestring):
1309       if opts.hvparams[param].lower() == "default":
1310         opts.hvparams[param] = constants.VALUE_DEFAULT
1311
1312   utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1313                       allowed_values=[constants.VALUE_DEFAULT])
1314
1315   nics = _ConvertNicDiskModifications(opts.nics)
1316   disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1317
1318   if (opts.disk_template and
1319       opts.disk_template in constants.DTS_INT_MIRROR and
1320       not opts.node):
1321     ToStderr("Changing the disk template to a mirrored one requires"
1322              " specifying a secondary node")
1323     return 1
1324
1325   if opts.offline_inst:
1326     offline = True
1327   elif opts.online_inst:
1328     offline = False
1329   else:
1330     offline = None
1331
1332   op = opcodes.OpInstanceSetParams(instance_name=args[0],
1333                                    nics=nics,
1334                                    disks=disks,
1335                                    disk_template=opts.disk_template,
1336                                    remote_node=opts.node,
1337                                    pnode=opts.new_primary_node,
1338                                    hvparams=opts.hvparams,
1339                                    beparams=opts.beparams,
1340                                    runtime_mem=opts.runtime_mem,
1341                                    os_name=opts.os,
1342                                    osparams=opts.osparams,
1343                                    force_variant=opts.force_variant,
1344                                    force=opts.force,
1345                                    wait_for_sync=opts.wait_for_sync,
1346                                    offline=offline,
1347                                    conflicts_check=opts.conflicts_check,
1348                                    ignore_ipolicy=opts.ignore_ipolicy)
1349
1350   # even if here we process the result, we allow submit only
1351   result = SubmitOrSend(op, opts)
1352
1353   if result:
1354     ToStdout("Modified instance %s", args[0])
1355     for param, data in result:
1356       ToStdout(" - %-5s -> %s", param, data)
1357     ToStdout("Please don't forget that most parameters take effect"
1358              " only at the next (re)start of the instance initiated by"
1359              " ganeti; restarting from within the instance will"
1360              " not be enough.")
1361   return 0
1362
1363
1364 def ChangeGroup(opts, args):
1365   """Moves an instance to another group.
1366
1367   """
1368   (instance_name, ) = args
1369
1370   cl = GetClient()
1371
1372   op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1373                                      iallocator=opts.iallocator,
1374                                      target_groups=opts.to,
1375                                      early_release=opts.early_release)
1376   result = SubmitOrSend(op, opts, cl=cl)
1377
1378   # Keep track of submitted jobs
1379   jex = JobExecutor(cl=cl, opts=opts)
1380
1381   for (status, job_id) in result[constants.JOB_IDS_KEY]:
1382     jex.AddJobId(None, status, job_id)
1383
1384   results = jex.GetResults()
1385   bad_cnt = len([row for row in results if not row[0]])
1386   if bad_cnt == 0:
1387     ToStdout("Instance '%s' changed group successfully.", instance_name)
1388     rcode = constants.EXIT_SUCCESS
1389   else:
1390     ToStdout("There were %s errors while changing group of instance '%s'.",
1391              bad_cnt, instance_name)
1392     rcode = constants.EXIT_FAILURE
1393
1394   return rcode
1395
1396
1397 # multi-instance selection options
1398 m_force_multi = cli_option("--force-multiple", dest="force_multi",
1399                            help="Do not ask for confirmation when more than"
1400                            " one instance is affected",
1401                            action="store_true", default=False)
1402
1403 m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1404                             help="Filter by nodes (primary only)",
1405                             const=_EXPAND_NODES_PRI, action="store_const")
1406
1407 m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1408                             help="Filter by nodes (secondary only)",
1409                             const=_EXPAND_NODES_SEC, action="store_const")
1410
1411 m_node_opt = cli_option("--node", dest="multi_mode",
1412                         help="Filter by nodes (primary and secondary)",
1413                         const=_EXPAND_NODES_BOTH, action="store_const")
1414
1415 m_clust_opt = cli_option("--all", dest="multi_mode",
1416                          help="Select all instances in the cluster",
1417                          const=_EXPAND_CLUSTER, action="store_const")
1418
1419 m_inst_opt = cli_option("--instance", dest="multi_mode",
1420                         help="Filter by instance name [default]",
1421                         const=_EXPAND_INSTANCES, action="store_const")
1422
1423 m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1424                              help="Filter by node tag",
1425                              const=_EXPAND_NODES_BOTH_BY_TAGS,
1426                              action="store_const")
1427
1428 m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1429                                  help="Filter by primary node tag",
1430                                  const=_EXPAND_NODES_PRI_BY_TAGS,
1431                                  action="store_const")
1432
1433 m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1434                                  help="Filter by secondary node tag",
1435                                  const=_EXPAND_NODES_SEC_BY_TAGS,
1436                                  action="store_const")
1437
1438 m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1439                              help="Filter by instance tag",
1440                              const=_EXPAND_INSTANCES_BY_TAGS,
1441                              action="store_const")
1442
1443 # this is defined separately due to readability only
1444 add_opts = [
1445   NOSTART_OPT,
1446   OS_OPT,
1447   FORCE_VARIANT_OPT,
1448   NO_INSTALL_OPT,
1449   IGNORE_IPOLICY_OPT,
1450   ]
1451
1452 commands = {
1453   "add": (
1454     AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
1455     "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1456     "Creates and adds a new instance to the cluster"),
1457   "batch-create": (
1458     BatchCreate, [ArgFile(min=1, max=1)],
1459     [DRY_RUN_OPT, PRIORITY_OPT, IALLOCATOR_OPT, SUBMIT_OPT],
1460     "<instances.json>",
1461     "Create a bunch of instances based on specs in the file."),
1462   "console": (
1463     ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1464     [SHOWCMD_OPT, PRIORITY_OPT],
1465     "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1466   "failover": (
1467     FailoverInstance, ARGS_ONE_INSTANCE,
1468     [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
1469      DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1470      IGNORE_IPOLICY_OPT],
1471     "[-f] <instance>", "Stops the instance, changes its primary node and"
1472     " (if it was originally running) starts it on the new node"
1473     " (the secondary for mirrored instances or any node"
1474     " for shared storage)."),
1475   "migrate": (
1476     MigrateInstance, ARGS_ONE_INSTANCE,
1477     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1478      PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1479      IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1480     "[-f] <instance>", "Migrate instance to its secondary node"
1481     " (only for mirrored instances)"),
1482   "move": (
1483     MoveInstance, ARGS_ONE_INSTANCE,
1484     [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
1485      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT, IGNORE_IPOLICY_OPT],
1486     "[-f] <instance>", "Move instance to an arbitrary node"
1487     " (only for instances of type file and lv)"),
1488   "info": (
1489     ShowInstanceConfig, ARGS_MANY_INSTANCES,
1490     [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1491     "[-s] {--all | <instance>...}",
1492     "Show information on the specified instance(s)"),
1493   "list": (
1494     ListInstances, ARGS_MANY_INSTANCES,
1495     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1496      FORCE_FILTER_OPT],
1497     "[<instance>...]",
1498     "Lists the instances and their status. The available fields can be shown"
1499     " using the \"list-fields\" command (see the man page for details)."
1500     " The default field list is (in order): %s." %
1501     utils.CommaJoin(_LIST_DEF_FIELDS),
1502     ),
1503   "list-fields": (
1504     ListInstanceFields, [ArgUnknown()],
1505     [NOHDR_OPT, SEP_OPT],
1506     "[fields...]",
1507     "Lists all available fields for instances"),
1508   "reinstall": (
1509     ReinstallInstance, [ArgInstance()],
1510     [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1511      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1512      m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1513      SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1514     "[-f] <instance>", "Reinstall a stopped instance"),
1515   "remove": (
1516     RemoveInstance, ARGS_ONE_INSTANCE,
1517     [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
1518      DRY_RUN_OPT, PRIORITY_OPT],
1519     "[-f] <instance>", "Shuts down the instance and removes it"),
1520   "rename": (
1521     RenameInstance,
1522     [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1523     [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1524     "<instance> <new_name>", "Rename the instance"),
1525   "replace-disks": (
1526     ReplaceDisks, ARGS_ONE_INSTANCE,
1527     [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1528      NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
1529      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1530     "[-s|-p|-a|-n NODE|-I NAME] <instance>",
1531     "Replaces disks for the instance"),
1532   "modify": (
1533     SetInstanceParams, ARGS_ONE_INSTANCE,
1534     [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
1535      DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1536      OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
1537      ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
1538      NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT],
1539     "<instance>", "Alters the parameters of an instance"),
1540   "shutdown": (
1541     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1542     [FORCE_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1543      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1544      m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
1545      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1546     "<instance>", "Stops an instance"),
1547   "startup": (
1548     GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1549     [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1550      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1551      m_inst_tags_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, HVOPTS_OPT,
1552      BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1553      NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1554     "<instance>", "Starts an instance"),
1555   "reboot": (
1556     GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1557     [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1558      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
1559      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1560      m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1561     "<instance>", "Reboots an instance"),
1562   "activate-disks": (
1563     ActivateDisks, ARGS_ONE_INSTANCE,
1564     [SUBMIT_OPT, IGNORE_SIZE_OPT, PRIORITY_OPT, WFSYNC_OPT],
1565     "<instance>", "Activate an instance's disks"),
1566   "deactivate-disks": (
1567     DeactivateDisks, ARGS_ONE_INSTANCE,
1568     [FORCE_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1569     "[-f] <instance>", "Deactivate an instance's disks"),
1570   "recreate-disks": (
1571     RecreateDisks, ARGS_ONE_INSTANCE,
1572     [SUBMIT_OPT, DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
1573      IALLOCATOR_OPT],
1574     "<instance>", "Recreate an instance's disks"),
1575   "grow-disk": (
1576     GrowDisk,
1577     [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1578      ArgUnknown(min=1, max=1)],
1579     [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
1580     "<instance> <disk> <size>", "Grow an instance's disk"),
1581   "change-group": (
1582     ChangeGroup, ARGS_ONE_INSTANCE,
1583     [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT, SUBMIT_OPT],
1584     "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1585   "list-tags": (
1586     ListTags, ARGS_ONE_INSTANCE, [],
1587     "<instance_name>", "List the tags of the given instance"),
1588   "add-tags": (
1589     AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1590     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1591     "<instance_name> tag...", "Add tags to the given instance"),
1592   "remove-tags": (
1593     RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1594     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1595     "<instance_name> tag...", "Remove tags from given instance"),
1596   }
1597
1598 #: dictionary with aliases for commands
1599 aliases = {
1600   "start": "startup",
1601   "stop": "shutdown",
1602   "show": "info",
1603   }
1604
1605
1606 def Main():
1607   return GenericMain(commands, aliases=aliases,
1608                      override={"tag_type": constants.TAG_INSTANCE},
1609                      env_override=_ENV_OVERRIDE)