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