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