gnt-instance info uses a revised format
[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                                   reason=(constants.INSTANCE_REASON_SOURCE_CLI,
635                                           opts.reason))
636
637
638 def _ShutdownInstance(name, opts):
639   """Shutdown an instance.
640
641   This returns the opcode to shutdown an instance, and its decorator
642   will wrap this into a loop shutting down all desired instances.
643
644   @param name: the name of the instance to act on
645   @param opts: the command line options selected by the user
646   @return: the opcode needed for the operation
647
648   """
649   return opcodes.OpInstanceShutdown(instance_name=name,
650                                     force=opts.force,
651                                     timeout=opts.timeout,
652                                     ignore_offline_nodes=opts.ignore_offline,
653                                     no_remember=opts.no_remember)
654
655
656 def ReplaceDisks(opts, args):
657   """Replace the disks of an instance
658
659   @param opts: the command line options selected by the user
660   @type args: list
661   @param args: should contain only one element, the instance name
662   @rtype: int
663   @return: the desired exit code
664
665   """
666   new_2ndary = opts.dst_node
667   iallocator = opts.iallocator
668   if opts.disks is None:
669     disks = []
670   else:
671     try:
672       disks = [int(i) for i in opts.disks.split(",")]
673     except (TypeError, ValueError), err:
674       raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
675                                  errors.ECODE_INVAL)
676   cnt = [opts.on_primary, opts.on_secondary, opts.auto,
677          new_2ndary is not None, iallocator is not None].count(True)
678   if cnt != 1:
679     raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
680                                " options must be passed", errors.ECODE_INVAL)
681   elif opts.on_primary:
682     mode = constants.REPLACE_DISK_PRI
683   elif opts.on_secondary:
684     mode = constants.REPLACE_DISK_SEC
685   elif opts.auto:
686     mode = constants.REPLACE_DISK_AUTO
687     if disks:
688       raise errors.OpPrereqError("Cannot specify disks when using automatic"
689                                  " mode", errors.ECODE_INVAL)
690   elif new_2ndary is not None or iallocator is not None:
691     # replace secondary
692     mode = constants.REPLACE_DISK_CHG
693
694   op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
695                                       remote_node=new_2ndary, mode=mode,
696                                       iallocator=iallocator,
697                                       early_release=opts.early_release,
698                                       ignore_ipolicy=opts.ignore_ipolicy)
699   SubmitOrSend(op, opts)
700   return 0
701
702
703 def FailoverInstance(opts, args):
704   """Failover an instance.
705
706   The failover is done by shutting it down on its present node and
707   starting it on the secondary.
708
709   @param opts: the command line options selected by the user
710   @type args: list
711   @param args: should contain only one element, the instance name
712   @rtype: int
713   @return: the desired exit code
714
715   """
716   cl = GetClient()
717   instance_name = args[0]
718   force = opts.force
719   iallocator = opts.iallocator
720   target_node = opts.dst_node
721
722   if iallocator and target_node:
723     raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
724                                " node (-n) but not both", errors.ECODE_INVAL)
725
726   if not force:
727     _EnsureInstancesExist(cl, [instance_name])
728
729     usertext = ("Failover will happen to image %s."
730                 " This requires a shutdown of the instance. Continue?" %
731                 (instance_name,))
732     if not AskUser(usertext):
733       return 1
734
735   op = opcodes.OpInstanceFailover(instance_name=instance_name,
736                                   ignore_consistency=opts.ignore_consistency,
737                                   shutdown_timeout=opts.shutdown_timeout,
738                                   iallocator=iallocator,
739                                   target_node=target_node,
740                                   ignore_ipolicy=opts.ignore_ipolicy)
741   SubmitOrSend(op, opts, cl=cl)
742   return 0
743
744
745 def MigrateInstance(opts, args):
746   """Migrate an instance.
747
748   The migrate is done without shutdown.
749
750   @param opts: the command line options selected by the user
751   @type args: list
752   @param args: should contain only one element, the instance name
753   @rtype: int
754   @return: the desired exit code
755
756   """
757   cl = GetClient()
758   instance_name = args[0]
759   force = opts.force
760   iallocator = opts.iallocator
761   target_node = opts.dst_node
762
763   if iallocator and target_node:
764     raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
765                                " node (-n) but not both", errors.ECODE_INVAL)
766
767   if not force:
768     _EnsureInstancesExist(cl, [instance_name])
769
770     if opts.cleanup:
771       usertext = ("Instance %s will be recovered from a failed migration."
772                   " Note that the migration procedure (including cleanup)" %
773                   (instance_name,))
774     else:
775       usertext = ("Instance %s will be migrated. Note that migration" %
776                   (instance_name,))
777     usertext += (" might impact the instance if anything goes wrong"
778                  " (e.g. due to bugs in the hypervisor). Continue?")
779     if not AskUser(usertext):
780       return 1
781
782   # this should be removed once --non-live is deprecated
783   if not opts.live and opts.migration_mode is not None:
784     raise errors.OpPrereqError("Only one of the --non-live and "
785                                "--migration-mode options can be passed",
786                                errors.ECODE_INVAL)
787   if not opts.live: # --non-live passed
788     mode = constants.HT_MIGRATION_NONLIVE
789   else:
790     mode = opts.migration_mode
791
792   op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
793                                  cleanup=opts.cleanup, iallocator=iallocator,
794                                  target_node=target_node,
795                                  allow_failover=opts.allow_failover,
796                                  allow_runtime_changes=opts.allow_runtime_chgs,
797                                  ignore_ipolicy=opts.ignore_ipolicy)
798   SubmitOrSend(op, cl=cl, opts=opts)
799   return 0
800
801
802 def MoveInstance(opts, args):
803   """Move an instance.
804
805   @param opts: the command line options selected by the user
806   @type args: list
807   @param args: should contain only one element, the instance name
808   @rtype: int
809   @return: the desired exit code
810
811   """
812   cl = GetClient()
813   instance_name = args[0]
814   force = opts.force
815
816   if not force:
817     usertext = ("Instance %s will be moved."
818                 " This requires a shutdown of the instance. Continue?" %
819                 (instance_name,))
820     if not AskUser(usertext):
821       return 1
822
823   op = opcodes.OpInstanceMove(instance_name=instance_name,
824                               target_node=opts.node,
825                               shutdown_timeout=opts.shutdown_timeout,
826                               ignore_consistency=opts.ignore_consistency,
827                               ignore_ipolicy=opts.ignore_ipolicy)
828   SubmitOrSend(op, opts, cl=cl)
829   return 0
830
831
832 def ConnectToInstanceConsole(opts, args):
833   """Connect to the console of an instance.
834
835   @param opts: the command line options selected by the user
836   @type args: list
837   @param args: should contain only one element, the instance name
838   @rtype: int
839   @return: the desired exit code
840
841   """
842   instance_name = args[0]
843
844   cl = GetClient()
845   try:
846     cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
847     ((console_data, oper_state), ) = \
848       cl.QueryInstances([instance_name], ["console", "oper_state"], False)
849   finally:
850     # Ensure client connection is closed while external commands are run
851     cl.Close()
852
853   del cl
854
855   if not console_data:
856     if oper_state:
857       # Instance is running
858       raise errors.OpExecError("Console information for instance %s is"
859                                " unavailable" % instance_name)
860     else:
861       raise errors.OpExecError("Instance %s is not running, can't get console" %
862                                instance_name)
863
864   return _DoConsole(objects.InstanceConsole.FromDict(console_data),
865                     opts.show_command, cluster_name)
866
867
868 def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
869                _runcmd_fn=utils.RunCmd):
870   """Acts based on the result of L{opcodes.OpInstanceConsole}.
871
872   @type console: L{objects.InstanceConsole}
873   @param console: Console object
874   @type show_command: bool
875   @param show_command: Whether to just display commands
876   @type cluster_name: string
877   @param cluster_name: Cluster name as retrieved from master daemon
878
879   """
880   assert console.Validate()
881
882   if console.kind == constants.CONS_MESSAGE:
883     feedback_fn(console.message)
884   elif console.kind == constants.CONS_VNC:
885     feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
886                 " URL <vnc://%s:%s/>",
887                 console.instance, console.host, console.port,
888                 console.display, console.host, console.port)
889   elif console.kind == constants.CONS_SPICE:
890     feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
891                 console.host, console.port)
892   elif console.kind == constants.CONS_SSH:
893     # Convert to string if not already one
894     if isinstance(console.command, basestring):
895       cmd = console.command
896     else:
897       cmd = utils.ShellQuoteArgs(console.command)
898
899     srun = ssh.SshRunner(cluster_name=cluster_name)
900     ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
901                             batch=True, quiet=False, tty=True)
902
903     if show_command:
904       feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
905     else:
906       result = _runcmd_fn(ssh_cmd, interactive=True)
907       if result.failed:
908         logging.error("Console command \"%s\" failed with reason '%s' and"
909                       " output %r", result.cmd, result.fail_reason,
910                       result.output)
911         raise errors.OpExecError("Connection to console of instance %s failed,"
912                                  " please check cluster configuration" %
913                                  console.instance)
914   else:
915     raise errors.GenericError("Unknown console type '%s'" % console.kind)
916
917   return constants.EXIT_SUCCESS
918
919
920 def _FormatLogicalID(dev_type, logical_id, roman):
921   """Formats the logical_id of a disk.
922
923   """
924   if dev_type == constants.LD_DRBD8:
925     node_a, node_b, port, minor_a, minor_b, key = logical_id
926     data = [
927       ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
928                                                             convert=roman))),
929       ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
930                                                             convert=roman))),
931       ("port", str(compat.TryToRoman(port, convert=roman))),
932       ("auth key", str(key)),
933       ]
934   elif dev_type == constants.LD_LV:
935     vg_name, lv_name = logical_id
936     data = ["%s/%s" % (vg_name, lv_name)]
937   else:
938     data = [str(logical_id)]
939
940   return data
941
942
943 def _FormatListInfo(data):
944   return list(str(i) for i in data)
945
946
947 def _FormatBlockDevInfo(idx, top_level, dev, roman):
948   """Show block device information.
949
950   This is only used by L{ShowInstanceConfig}, but it's too big to be
951   left for an inline definition.
952
953   @type idx: int
954   @param idx: the index of the current disk
955   @type top_level: boolean
956   @param top_level: if this a top-level disk?
957   @type dev: dict
958   @param dev: dictionary with disk information
959   @type roman: boolean
960   @param roman: whether to try to use roman integers
961   @return: a list of either strings, tuples or lists
962       (which should be formatted at a higher indent level)
963
964   """
965   def helper(dtype, status):
966     """Format one line for physical device status.
967
968     @type dtype: str
969     @param dtype: a constant from the L{constants.LDS_BLOCK} set
970     @type status: tuple
971     @param status: a tuple as returned from L{backend.FindBlockDevice}
972     @return: the string representing the status
973
974     """
975     if not status:
976       return "not active"
977     txt = ""
978     (path, major, minor, syncp, estt, degr, ldisk_status) = status
979     if major is None:
980       major_string = "N/A"
981     else:
982       major_string = str(compat.TryToRoman(major, convert=roman))
983
984     if minor is None:
985       minor_string = "N/A"
986     else:
987       minor_string = str(compat.TryToRoman(minor, convert=roman))
988
989     txt += ("%s (%s:%s)" % (path, major_string, minor_string))
990     if dtype in (constants.LD_DRBD8, ):
991       if syncp is not None:
992         sync_text = "*RECOVERING* %5.2f%%," % syncp
993         if estt:
994           sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
995         else:
996           sync_text += " ETA unknown"
997       else:
998         sync_text = "in sync"
999       if degr:
1000         degr_text = "*DEGRADED*"
1001       else:
1002         degr_text = "ok"
1003       if ldisk_status == constants.LDS_FAULTY:
1004         ldisk_text = " *MISSING DISK*"
1005       elif ldisk_status == constants.LDS_UNKNOWN:
1006         ldisk_text = " *UNCERTAIN STATE*"
1007       else:
1008         ldisk_text = ""
1009       txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1010     elif dtype == constants.LD_LV:
1011       if ldisk_status == constants.LDS_FAULTY:
1012         ldisk_text = " *FAILED* (failed drive?)"
1013       else:
1014         ldisk_text = ""
1015       txt += ldisk_text
1016     return txt
1017
1018   # the header
1019   if top_level:
1020     if dev["iv_name"] is not None:
1021       txt = dev["iv_name"]
1022     else:
1023       txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1024   else:
1025     txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1026   if isinstance(dev["size"], int):
1027     nice_size = utils.FormatUnit(dev["size"], "h")
1028   else:
1029     nice_size = str(dev["size"])
1030   data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1031   if top_level:
1032     data.append(("access mode", dev["mode"]))
1033   if dev["logical_id"] is not None:
1034     try:
1035       l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], 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   elif dev["physical_id"] is not None:
1043     data.append(("physical_id:", _FormatListInfo(dev["physical_id"])))
1044
1045   if dev["pstatus"]:
1046     data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1047
1048   if dev["sstatus"]:
1049     data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1050
1051   if dev["children"]:
1052     data.append(("child devices", [
1053       _FormatBlockDevInfo(c_idx, False, child, roman)
1054       for c_idx, child in enumerate(dev["children"])
1055       ]))
1056   return data
1057
1058
1059 def _FormatInstanceNicInfo(idx, nic):
1060   """Helper function for L{_FormatInstanceInfo()}"""
1061   (ip, mac, mode, link, _, netinfo) = nic
1062   network_name = None
1063   if netinfo:
1064     network_name = netinfo["name"]
1065   return [
1066     ("nic/%d" % idx, ""),
1067     ("MAC", str(mac)),
1068     ("IP", str(ip)),
1069     ("mode", str(mode)),
1070     ("link", str(link)),
1071     ("network", str(network_name)),
1072     ]
1073
1074
1075 def _FormatInstanceNodesInfo(instance):
1076   """Helper function for L{_FormatInstanceInfo()}"""
1077   pgroup = ("%s (UUID %s)" %
1078             (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1079   secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1080                           (name, group_name, group_uuid))
1081                          for (name, group_name, group_uuid) in
1082                            zip(instance["snodes"],
1083                                instance["snodes_group_names"],
1084                                instance["snodes_group_uuids"]))
1085   return [
1086     [
1087       ("primary", instance["pnode"]),
1088       ("group", pgroup),
1089       ],
1090     [("secondaries", secs)],
1091     ]
1092
1093
1094 def _GetVncConsoleInfo(instance):
1095   """Helper function for L{_FormatInstanceInfo()}"""
1096   vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1097                                                None)
1098   if vnc_bind_address:
1099     port = instance["network_port"]
1100     display = int(port) - constants.VNC_BASE_PORT
1101     if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1102       vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1103                                                  port,
1104                                                  display)
1105     elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1106       vnc_console_port = ("%s:%s (node %s) (display %s)" %
1107                            (vnc_bind_address, port,
1108                             instance["pnode"], display))
1109     else:
1110       # vnc bind address is a file
1111       vnc_console_port = "%s:%s" % (instance["pnode"],
1112                                     vnc_bind_address)
1113     ret = "vnc to %s" % vnc_console_port
1114   else:
1115     ret = None
1116   return ret
1117
1118
1119 def _FormatInstanceInfo(instance, roman_integers):
1120   """Format instance information for L{cli.PrintGenericInfo()}"""
1121   istate = "configured to be %s" % instance["config_state"]
1122   if instance["run_state"]:
1123     istate += ", actual state is %s" % instance["run_state"]
1124   info = [
1125     ("Instance name", instance["name"]),
1126     ("UUID", instance["uuid"]),
1127     ("Serial number",
1128      str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1129     ("Creation time", utils.FormatTime(instance["ctime"])),
1130     ("Modification time", utils.FormatTime(instance["mtime"])),
1131     ("State", istate),
1132     ("Nodes", _FormatInstanceNodesInfo(instance)),
1133     ("Operating system", instance["os"]),
1134     ("Operating system parameters",
1135      FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1136     ]
1137
1138   if "network_port" in instance:
1139     info.append(("Allocated network port",
1140                  str(compat.TryToRoman(instance["network_port"],
1141                                        convert=roman_integers))))
1142   info.append(("Hypervisor", instance["hypervisor"]))
1143   console = _GetVncConsoleInfo(instance)
1144   if console:
1145     info.append(("console connection", console))
1146   # deprecated "memory" value, kept for one version for compatibility
1147   # TODO(ganeti 2.7) remove.
1148   be_actual = copy.deepcopy(instance["be_actual"])
1149   be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1150   info.extend([
1151     ("Hypervisor parameters",
1152      FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1153     ("Back-end parameters",
1154      FormatParamsDictInfo(instance["be_instance"], be_actual)),
1155     ("NICs", [
1156       _FormatInstanceNicInfo(idx, nic)
1157       for (idx, nic) in enumerate(instance["nics"])
1158       ]),
1159     ("Disk template", instance["disk_template"]),
1160     ("Disks", [
1161       _FormatBlockDevInfo(idx, True, device, roman_integers)
1162       for (idx, device) in enumerate(instance["disks"])
1163       ]),
1164     ])
1165   return info
1166
1167
1168 def ShowInstanceConfig(opts, args):
1169   """Compute instance run-time status.
1170
1171   @param opts: the command line options selected by the user
1172   @type args: list
1173   @param args: either an empty list, and then we query all
1174       instances, or should contain a list of instance names
1175   @rtype: int
1176   @return: the desired exit code
1177
1178   """
1179   if not args and not opts.show_all:
1180     ToStderr("No instance selected."
1181              " Please pass in --all if you want to query all instances.\n"
1182              "Note that this can take a long time on a big cluster.")
1183     return 1
1184   elif args and opts.show_all:
1185     ToStderr("Cannot use --all if you specify instance names.")
1186     return 1
1187
1188   retcode = 0
1189   op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1190                                    use_locking=not opts.static)
1191   result = SubmitOpCode(op, opts=opts)
1192   if not result:
1193     ToStdout("No instances.")
1194     return 1
1195
1196   PrintGenericInfo([
1197     _FormatInstanceInfo(instance, opts.roman_integers)
1198     for instance in result.values()
1199     ])
1200   return retcode
1201
1202
1203 def _ConvertNicDiskModifications(mods):
1204   """Converts NIC/disk modifications from CLI to opcode.
1205
1206   When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
1207   disks at arbitrary indices, its parameter format changed. This function
1208   converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
1209   newer format and adds support for new-style requests (e.g. "--new 4:add").
1210
1211   @type mods: list of tuples
1212   @param mods: Modifications as given by command line parser
1213   @rtype: list of tuples
1214   @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1215
1216   """
1217   result = []
1218
1219   for (idx, params) in mods:
1220     if idx == constants.DDM_ADD:
1221       # Add item as last item (legacy interface)
1222       action = constants.DDM_ADD
1223       idxno = -1
1224     elif idx == constants.DDM_REMOVE:
1225       # Remove last item (legacy interface)
1226       action = constants.DDM_REMOVE
1227       idxno = -1
1228     else:
1229       # Modifications and adding/removing at arbitrary indices
1230       try:
1231         idxno = int(idx)
1232       except (TypeError, ValueError):
1233         raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
1234                                    errors.ECODE_INVAL)
1235
1236       add = params.pop(constants.DDM_ADD, _MISSING)
1237       remove = params.pop(constants.DDM_REMOVE, _MISSING)
1238       modify = params.pop(constants.DDM_MODIFY, _MISSING)
1239
1240       if modify is _MISSING:
1241         if not (add is _MISSING or remove is _MISSING):
1242           raise errors.OpPrereqError("Cannot add and remove at the same time",
1243                                      errors.ECODE_INVAL)
1244         elif add is not _MISSING:
1245           action = constants.DDM_ADD
1246         elif remove is not _MISSING:
1247           action = constants.DDM_REMOVE
1248         else:
1249           action = constants.DDM_MODIFY
1250
1251       elif add is _MISSING and remove is _MISSING:
1252         action = constants.DDM_MODIFY
1253       else:
1254         raise errors.OpPrereqError("Cannot modify and add/remove at the"
1255                                    " same time", errors.ECODE_INVAL)
1256
1257       assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1258
1259     if action == constants.DDM_REMOVE and params:
1260       raise errors.OpPrereqError("Not accepting parameters on removal",
1261                                  errors.ECODE_INVAL)
1262
1263     result.append((action, idxno, params))
1264
1265   return result
1266
1267
1268 def _ParseDiskSizes(mods):
1269   """Parses disk sizes in parameters.
1270
1271   """
1272   for (action, _, params) in mods:
1273     if params and constants.IDISK_SIZE in params:
1274       params[constants.IDISK_SIZE] = \
1275         utils.ParseUnit(params[constants.IDISK_SIZE])
1276     elif action == constants.DDM_ADD:
1277       raise errors.OpPrereqError("Missing required parameter 'size'",
1278                                  errors.ECODE_INVAL)
1279
1280   return mods
1281
1282
1283 def SetInstanceParams(opts, args):
1284   """Modifies an instance.
1285
1286   All parameters take effect only at the next restart of the instance.
1287
1288   @param opts: the command line options selected by the user
1289   @type args: list
1290   @param args: should contain only one element, the instance name
1291   @rtype: int
1292   @return: the desired exit code
1293
1294   """
1295   if not (opts.nics or opts.disks or opts.disk_template or
1296           opts.hvparams or opts.beparams or opts.os or opts.osparams or
1297           opts.offline_inst or opts.online_inst or opts.runtime_mem):
1298     ToStderr("Please give at least one of the parameters.")
1299     return 1
1300
1301   for param in opts.beparams:
1302     if isinstance(opts.beparams[param], basestring):
1303       if opts.beparams[param].lower() == "default":
1304         opts.beparams[param] = constants.VALUE_DEFAULT
1305
1306   utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1307                       allowed_values=[constants.VALUE_DEFAULT])
1308
1309   for param in opts.hvparams:
1310     if isinstance(opts.hvparams[param], basestring):
1311       if opts.hvparams[param].lower() == "default":
1312         opts.hvparams[param] = constants.VALUE_DEFAULT
1313
1314   utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1315                       allowed_values=[constants.VALUE_DEFAULT])
1316
1317   nics = _ConvertNicDiskModifications(opts.nics)
1318   disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1319
1320   if (opts.disk_template and
1321       opts.disk_template in constants.DTS_INT_MIRROR and
1322       not opts.node):
1323     ToStderr("Changing the disk template to a mirrored one requires"
1324              " specifying a secondary node")
1325     return 1
1326
1327   if opts.offline_inst:
1328     offline = True
1329   elif opts.online_inst:
1330     offline = False
1331   else:
1332     offline = None
1333
1334   op = opcodes.OpInstanceSetParams(instance_name=args[0],
1335                                    nics=nics,
1336                                    disks=disks,
1337                                    disk_template=opts.disk_template,
1338                                    remote_node=opts.node,
1339                                    hvparams=opts.hvparams,
1340                                    beparams=opts.beparams,
1341                                    runtime_mem=opts.runtime_mem,
1342                                    os_name=opts.os,
1343                                    osparams=opts.osparams,
1344                                    force_variant=opts.force_variant,
1345                                    force=opts.force,
1346                                    wait_for_sync=opts.wait_for_sync,
1347                                    offline=offline,
1348                                    conflicts_check=opts.conflicts_check,
1349                                    ignore_ipolicy=opts.ignore_ipolicy)
1350
1351   # even if here we process the result, we allow submit only
1352   result = SubmitOrSend(op, opts)
1353
1354   if result:
1355     ToStdout("Modified instance %s", args[0])
1356     for param, data in result:
1357       ToStdout(" - %-5s -> %s", param, data)
1358     ToStdout("Please don't forget that most parameters take effect"
1359              " only at the next (re)start of the instance initiated by"
1360              " ganeti; restarting from within the instance will"
1361              " not be enough.")
1362   return 0
1363
1364
1365 def ChangeGroup(opts, args):
1366   """Moves an instance to another group.
1367
1368   """
1369   (instance_name, ) = args
1370
1371   cl = GetClient()
1372
1373   op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1374                                      iallocator=opts.iallocator,
1375                                      target_groups=opts.to,
1376                                      early_release=opts.early_release)
1377   result = SubmitOrSend(op, opts, cl=cl)
1378
1379   # Keep track of submitted jobs
1380   jex = JobExecutor(cl=cl, opts=opts)
1381
1382   for (status, job_id) in result[constants.JOB_IDS_KEY]:
1383     jex.AddJobId(None, status, job_id)
1384
1385   results = jex.GetResults()
1386   bad_cnt = len([row for row in results if not row[0]])
1387   if bad_cnt == 0:
1388     ToStdout("Instance '%s' changed group successfully.", instance_name)
1389     rcode = constants.EXIT_SUCCESS
1390   else:
1391     ToStdout("There were %s errors while changing group of instance '%s'.",
1392              bad_cnt, instance_name)
1393     rcode = constants.EXIT_FAILURE
1394
1395   return rcode
1396
1397
1398 # multi-instance selection options
1399 m_force_multi = cli_option("--force-multiple", dest="force_multi",
1400                            help="Do not ask for confirmation when more than"
1401                            " one instance is affected",
1402                            action="store_true", default=False)
1403
1404 m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1405                             help="Filter by nodes (primary only)",
1406                             const=_EXPAND_NODES_PRI, action="store_const")
1407
1408 m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1409                             help="Filter by nodes (secondary only)",
1410                             const=_EXPAND_NODES_SEC, action="store_const")
1411
1412 m_node_opt = cli_option("--node", dest="multi_mode",
1413                         help="Filter by nodes (primary and secondary)",
1414                         const=_EXPAND_NODES_BOTH, action="store_const")
1415
1416 m_clust_opt = cli_option("--all", dest="multi_mode",
1417                          help="Select all instances in the cluster",
1418                          const=_EXPAND_CLUSTER, action="store_const")
1419
1420 m_inst_opt = cli_option("--instance", dest="multi_mode",
1421                         help="Filter by instance name [default]",
1422                         const=_EXPAND_INSTANCES, action="store_const")
1423
1424 m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1425                              help="Filter by node tag",
1426                              const=_EXPAND_NODES_BOTH_BY_TAGS,
1427                              action="store_const")
1428
1429 m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1430                                  help="Filter by primary node tag",
1431                                  const=_EXPAND_NODES_PRI_BY_TAGS,
1432                                  action="store_const")
1433
1434 m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1435                                  help="Filter by secondary node tag",
1436                                  const=_EXPAND_NODES_SEC_BY_TAGS,
1437                                  action="store_const")
1438
1439 m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1440                              help="Filter by instance tag",
1441                              const=_EXPAND_INSTANCES_BY_TAGS,
1442                              action="store_const")
1443
1444 # this is defined separately due to readability only
1445 add_opts = [
1446   NOSTART_OPT,
1447   OS_OPT,
1448   FORCE_VARIANT_OPT,
1449   NO_INSTALL_OPT,
1450   IGNORE_IPOLICY_OPT,
1451   ]
1452
1453 commands = {
1454   "add": (
1455     AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
1456     "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1457     "Creates and adds a new instance to the cluster"),
1458   "batch-create": (
1459     BatchCreate, [ArgFile(min=1, max=1)],
1460     [DRY_RUN_OPT, PRIORITY_OPT, IALLOCATOR_OPT, SUBMIT_OPT],
1461     "<instances.json>",
1462     "Create a bunch of instances based on specs in the file."),
1463   "console": (
1464     ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1465     [SHOWCMD_OPT, PRIORITY_OPT],
1466     "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1467   "failover": (
1468     FailoverInstance, ARGS_ONE_INSTANCE,
1469     [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
1470      DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1471      IGNORE_IPOLICY_OPT],
1472     "[-f] <instance>", "Stops the instance, changes its primary node and"
1473     " (if it was originally running) starts it on the new node"
1474     " (the secondary for mirrored instances or any node"
1475     " for shared storage)."),
1476   "migrate": (
1477     MigrateInstance, ARGS_ONE_INSTANCE,
1478     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1479      PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1480      IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1481     "[-f] <instance>", "Migrate instance to its secondary node"
1482     " (only for mirrored instances)"),
1483   "move": (
1484     MoveInstance, ARGS_ONE_INSTANCE,
1485     [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
1486      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT, IGNORE_IPOLICY_OPT],
1487     "[-f] <instance>", "Move instance to an arbitrary node"
1488     " (only for instances of type file and lv)"),
1489   "info": (
1490     ShowInstanceConfig, ARGS_MANY_INSTANCES,
1491     [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1492     "[-s] {--all | <instance>...}",
1493     "Show information on the specified instance(s)"),
1494   "list": (
1495     ListInstances, ARGS_MANY_INSTANCES,
1496     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1497      FORCE_FILTER_OPT],
1498     "[<instance>...]",
1499     "Lists the instances and their status. The available fields can be shown"
1500     " using the \"list-fields\" command (see the man page for details)."
1501     " The default field list is (in order): %s." %
1502     utils.CommaJoin(_LIST_DEF_FIELDS),
1503     ),
1504   "list-fields": (
1505     ListInstanceFields, [ArgUnknown()],
1506     [NOHDR_OPT, SEP_OPT],
1507     "[fields...]",
1508     "Lists all available fields for instances"),
1509   "reinstall": (
1510     ReinstallInstance, [ArgInstance()],
1511     [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1512      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1513      m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1514      SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1515     "[-f] <instance>", "Reinstall a stopped instance"),
1516   "remove": (
1517     RemoveInstance, ARGS_ONE_INSTANCE,
1518     [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
1519      DRY_RUN_OPT, PRIORITY_OPT],
1520     "[-f] <instance>", "Shuts down the instance and removes it"),
1521   "rename": (
1522     RenameInstance,
1523     [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1524     [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1525     "<instance> <new_name>", "Rename the instance"),
1526   "replace-disks": (
1527     ReplaceDisks, ARGS_ONE_INSTANCE,
1528     [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1529      NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
1530      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1531     "[-s|-p|-a|-n NODE|-I NAME] <instance>",
1532     "Replaces disks for the instance"),
1533   "modify": (
1534     SetInstanceParams, ARGS_ONE_INSTANCE,
1535     [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
1536      DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1537      OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
1538      ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
1539      NOCONFLICTSCHECK_OPT],
1540     "<instance>", "Alters the parameters of an instance"),
1541   "shutdown": (
1542     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1543     [FORCE_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1544      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1545      m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
1546      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1547     "<instance>", "Stops an instance"),
1548   "startup": (
1549     GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1550     [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1551      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1552      m_inst_tags_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, HVOPTS_OPT,
1553      BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1554      NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1555     "<instance>", "Starts an instance"),
1556   "reboot": (
1557     GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1558     [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1559      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
1560      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1561      m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
1562      REASON_OPT],
1563     "<instance>", "Reboots an instance"),
1564   "activate-disks": (
1565     ActivateDisks, ARGS_ONE_INSTANCE,
1566     [SUBMIT_OPT, IGNORE_SIZE_OPT, PRIORITY_OPT, WFSYNC_OPT],
1567     "<instance>", "Activate an instance's disks"),
1568   "deactivate-disks": (
1569     DeactivateDisks, ARGS_ONE_INSTANCE,
1570     [FORCE_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1571     "[-f] <instance>", "Deactivate an instance's disks"),
1572   "recreate-disks": (
1573     RecreateDisks, ARGS_ONE_INSTANCE,
1574     [SUBMIT_OPT, DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
1575      IALLOCATOR_OPT],
1576     "<instance>", "Recreate an instance's disks"),
1577   "grow-disk": (
1578     GrowDisk,
1579     [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1580      ArgUnknown(min=1, max=1)],
1581     [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
1582     "<instance> <disk> <size>", "Grow an instance's disk"),
1583   "change-group": (
1584     ChangeGroup, ARGS_ONE_INSTANCE,
1585     [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT, SUBMIT_OPT],
1586     "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1587   "list-tags": (
1588     ListTags, ARGS_ONE_INSTANCE, [],
1589     "<instance_name>", "List the tags of the given instance"),
1590   "add-tags": (
1591     AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1592     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1593     "<instance_name> tag...", "Add tags to the given instance"),
1594   "remove-tags": (
1595     RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1596     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1597     "<instance_name> tag...", "Remove tags from given instance"),
1598   }
1599
1600 #: dictionary with aliases for commands
1601 aliases = {
1602   "start": "startup",
1603   "stop": "shutdown",
1604   "show": "info",
1605   }
1606
1607
1608 def Main():
1609   return GenericMain(commands, aliases=aliases,
1610                      override={"tag_type": constants.TAG_INSTANCE},
1611                      env_override=_ENV_OVERRIDE)