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