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