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