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