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