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