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