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