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