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