Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 93f1e606

History | View | Annotate | Download (57.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2014 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Instance related commands"""
22

    
23
# pylint: disable=W0401,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0614: Unused import %s from wildcard import (since we need cli)
26
# C0103: Invalid name gnt-instance
27

    
28
import copy
29
import itertools
30
import simplejson
31
import logging
32

    
33
from ganeti.cli import *
34
from ganeti import opcodes
35
from ganeti import constants
36
from ganeti import compat
37
from ganeti import utils
38
from ganeti import errors
39
from ganeti import netutils
40
from ganeti import ssh
41
from ganeti import objects
42
from ganeti import ht
43

    
44

    
45
_EXPAND_CLUSTER = "cluster"
46
_EXPAND_NODES_BOTH = "nodes"
47
_EXPAND_NODES_PRI = "nodes-pri"
48
_EXPAND_NODES_SEC = "nodes-sec"
49
_EXPAND_NODES_BOTH_BY_TAGS = "nodes-by-tags"
50
_EXPAND_NODES_PRI_BY_TAGS = "nodes-pri-by-tags"
51
_EXPAND_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
52
_EXPAND_INSTANCES = "instances"
53
_EXPAND_INSTANCES_BY_TAGS = "instances-by-tags"
54

    
55
_EXPAND_NODES_TAGS_MODES = compat.UniqueFrozenset([
56
  _EXPAND_NODES_BOTH_BY_TAGS,
57
  _EXPAND_NODES_PRI_BY_TAGS,
58
  _EXPAND_NODES_SEC_BY_TAGS,
59
  ])
60

    
61
#: default list of options for L{ListInstances}
62
_LIST_DEF_FIELDS = [
63
  "name", "hypervisor", "os", "pnode", "status", "oper_ram",
64
  ]
65

    
66
_MISSING = object()
67
_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
68

    
69
_INST_DATA_VAL = ht.TListOf(ht.TDict)
70

    
71

    
72
def _ExpandMultiNames(mode, names, client=None):
73
  """Expand the given names using the passed mode.
74

75
  For _EXPAND_CLUSTER, all instances will be returned. For
76
  _EXPAND_NODES_PRI/SEC, all instances having those nodes as
77
  primary/secondary will be returned. For _EXPAND_NODES_BOTH, all
78
  instances having those nodes as either primary or secondary will be
79
  returned. For _EXPAND_INSTANCES, the given instances will be
80
  returned.
81

82
  @param mode: one of L{_EXPAND_CLUSTER}, L{_EXPAND_NODES_BOTH},
83
      L{_EXPAND_NODES_PRI}, L{_EXPAND_NODES_SEC} or
84
      L{_EXPAND_INSTANCES}
85
  @param names: a list of names; for cluster, it must be empty,
86
      and for node and instance it must be a list of valid item
87
      names (short names are valid as usual, e.g. node1 instead of
88
      node1.example.com)
89
  @rtype: list
90
  @return: the list of names after the expansion
91
  @raise errors.ProgrammerError: for unknown selection type
92
  @raise errors.OpPrereqError: for invalid input parameters
93

94
  """
95
  # pylint: disable=W0142
96

    
97
  if client is None:
98
    client = GetClient()
99
  if mode == _EXPAND_CLUSTER:
100
    if names:
101
      raise errors.OpPrereqError("Cluster filter mode takes no arguments",
102
                                 errors.ECODE_INVAL)
103
    idata = client.QueryInstances([], ["name"], False)
104
    inames = [row[0] for row in idata]
105

    
106
  elif (mode in _EXPAND_NODES_TAGS_MODES or
107
        mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_PRI, _EXPAND_NODES_SEC)):
108
    if mode in _EXPAND_NODES_TAGS_MODES:
109
      if not names:
110
        raise errors.OpPrereqError("No node tags passed", errors.ECODE_INVAL)
111
      ndata = client.QueryNodes([], ["name", "pinst_list",
112
                                     "sinst_list", "tags"], False)
113
      ndata = [row for row in ndata if set(row[3]).intersection(names)]
114
    else:
115
      if not names:
116
        raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
117
      ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
118
                                False)
119

    
120
    ipri = [row[1] for row in ndata]
121
    pri_names = list(itertools.chain(*ipri))
122
    isec = [row[2] for row in ndata]
123
    sec_names = list(itertools.chain(*isec))
124
    if mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_BOTH_BY_TAGS):
125
      inames = pri_names + sec_names
126
    elif mode in (_EXPAND_NODES_PRI, _EXPAND_NODES_PRI_BY_TAGS):
127
      inames = pri_names
128
    elif mode in (_EXPAND_NODES_SEC, _EXPAND_NODES_SEC_BY_TAGS):
129
      inames = sec_names
130
    else:
131
      raise errors.ProgrammerError("Unhandled shutdown type")
132
  elif mode == _EXPAND_INSTANCES:
133
    if not names:
134
      raise errors.OpPrereqError("No instance names passed",
135
                                 errors.ECODE_INVAL)
136
    idata = client.QueryInstances(names, ["name"], False)
137
    inames = [row[0] for row in idata]
138
  elif mode == _EXPAND_INSTANCES_BY_TAGS:
139
    if not names:
140
      raise errors.OpPrereqError("No instance tags passed",
141
                                 errors.ECODE_INVAL)
142
    idata = client.QueryInstances([], ["name", "tags"], False)
143
    inames = [row[0] for row in idata if set(row[1]).intersection(names)]
144
  else:
145
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
146

    
147
  return inames
148

    
149

    
150
def _EnsureInstancesExist(client, names):
151
  """Check for and ensure the given instance names exist.
152

153
  This function will raise an OpPrereqError in case they don't
154
  exist. Otherwise it will exit cleanly.
155

156
  @type client: L{ganeti.luxi.Client}
157
  @param client: the client to use for the query
158
  @type names: list
159
  @param names: the list of instance names to query
160
  @raise errors.OpPrereqError: in case any instance is missing
161

162
  """
163
  # TODO: change LUInstanceQuery to that it actually returns None
164
  # instead of raising an exception, or devise a better mechanism
165
  result = client.QueryInstances(names, ["name"], False)
166
  for orig_name, row in zip(names, result):
167
    if row[0] is None:
168
      raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
169
                                 errors.ECODE_NOENT)
170

    
171

    
172
def GenericManyOps(operation, fn):
173
  """Generic multi-instance operations.
174

175
  The will return a wrapper that processes the options and arguments
176
  given, and uses the passed function to build the opcode needed for
177
  the specific operation. Thus all the generic loop/confirmation code
178
  is abstracted into this function.
179

180
  """
181
  def realfn(opts, args):
182
    if opts.multi_mode is None:
183
      opts.multi_mode = _EXPAND_INSTANCES
184
    cl = GetClient()
185
    inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
186
    if not inames:
187
      if opts.multi_mode == _EXPAND_CLUSTER:
188
        ToStdout("Cluster is empty, no instances to shutdown")
189
        return 0
190
      raise errors.OpPrereqError("Selection filter does not match"
191
                                 " any instances", errors.ECODE_INVAL)
192
    multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
193
    if not (opts.force_multi or not multi_on
194
            or ConfirmOperation(inames, "instances", operation)):
195
      return 1
196
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
197
    for name in inames:
198
      op = fn(name, opts)
199
      jex.QueueJob(name, op)
200
    results = jex.WaitOrShow(not opts.submit_only)
201
    rcode = compat.all(row[0] for row in results)
202
    return int(not rcode)
203
  return realfn
204

    
205

    
206
def ListInstances(opts, args):
207
  """List instances and their properties.
208

209
  @param opts: the command line options selected by the user
210
  @type args: list
211
  @param args: should be an empty list
212
  @rtype: int
213
  @return: the desired exit code
214

215
  """
216
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
217

    
218
  fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
219
                               "nic.modes", "nic.links", "nic.bridges",
220
                               "nic.networks",
221
                               "snodes", "snodes.group", "snodes.group.uuid"],
222
                              (lambda value: ",".join(str(item)
223
                                                      for item in value),
224
                               False))
225

    
226
  cl = GetClient()
227

    
228
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
229
                     opts.separator, not opts.no_headers,
230
                     format_override=fmtoverride, verbose=opts.verbose,
231
                     force_filter=opts.force_filter, cl=cl)
232

    
233

    
234
def ListInstanceFields(opts, args):
235
  """List instance fields.
236

237
  @param opts: the command line options selected by the user
238
  @type args: list
239
  @param args: fields to list, or empty for all
240
  @rtype: int
241
  @return: the desired exit code
242

243
  """
244
  return GenericListFields(constants.QR_INSTANCE, args, opts.separator,
245
                           not opts.no_headers)
246

    
247

    
248
def AddInstance(opts, args):
249
  """Add an instance to the cluster.
250

251
  This is just a wrapper over L{GenericInstanceCreate}.
252

253
  """
254
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
255

    
256

    
257
def BatchCreate(opts, args):
258
  """Create instances using a definition file.
259

260
  This function reads a json file with L{opcodes.OpInstanceCreate}
261
  serialisations.
262

263
  @param opts: the command line options selected by the user
264
  @type args: list
265
  @param args: should contain one element, the json filename
266
  @rtype: int
267
  @return: the desired exit code
268

269
  """
270
  (json_filename,) = args
271
  cl = GetClient()
272

    
273
  try:
274
    instance_data = simplejson.loads(utils.ReadFile(json_filename))
275
  except Exception, err: # pylint: disable=W0703
276
    ToStderr("Can't parse the instance definition file: %s" % str(err))
277
    return 1
278

    
279
  if not _INST_DATA_VAL(instance_data):
280
    ToStderr("The instance definition file is not %s" % _INST_DATA_VAL)
281
    return 1
282

    
283
  instances = []
284
  possible_params = set(opcodes.OpInstanceCreate.GetAllSlots())
285
  for (idx, inst) in enumerate(instance_data):
286
    unknown = set(inst.keys()) - possible_params
287

    
288
    if unknown:
289
      # TODO: Suggest closest match for more user friendly experience
290
      raise errors.OpPrereqError("Unknown fields in definition %s: %s" %
291
                                 (idx, utils.CommaJoin(unknown)),
292
                                 errors.ECODE_INVAL)
293

    
294
    op = opcodes.OpInstanceCreate(**inst) # pylint: disable=W0142
295
    op.Validate(False)
296
    instances.append(op)
297

    
298
  op = opcodes.OpInstanceMultiAlloc(iallocator=opts.iallocator,
299
                                    instances=instances)
300
  result = SubmitOrSend(op, opts, cl=cl)
301

    
302
  # Keep track of submitted jobs
303
  jex = JobExecutor(cl=cl, opts=opts)
304

    
305
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
306
    jex.AddJobId(None, status, job_id)
307

    
308
  results = jex.GetResults()
309
  bad_cnt = len([row for row in results if not row[0]])
310
  if bad_cnt == 0:
311
    ToStdout("All instances created successfully.")
312
    rcode = constants.EXIT_SUCCESS
313
  else:
314
    ToStdout("There were %s errors during the creation.", bad_cnt)
315
    rcode = constants.EXIT_FAILURE
316

    
317
  return rcode
318

    
319

    
320
def ReinstallInstance(opts, args):
321
  """Reinstall an instance.
322

323
  @param opts: the command line options selected by the user
324
  @type args: list
325
  @param args: should contain only one element, the name of the
326
      instance to be reinstalled
327
  @rtype: int
328
  @return: the desired exit code
329

330
  """
331
  # first, compute the desired name list
332
  if opts.multi_mode is None:
333
    opts.multi_mode = _EXPAND_INSTANCES
334

    
335
  inames = _ExpandMultiNames(opts.multi_mode, args)
336
  if not inames:
337
    raise errors.OpPrereqError("Selection filter does not match any instances",
338
                               errors.ECODE_INVAL)
339

    
340
  # second, if requested, ask for an OS
341
  if opts.select_os is True:
342
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
343
    result = SubmitOpCode(op, opts=opts)
344

    
345
    if not result:
346
      ToStdout("Can't get the OS list")
347
      return 1
348

    
349
    ToStdout("Available OS templates:")
350
    number = 0
351
    choices = []
352
    for (name, variants) in result:
353
      for entry in CalculateOSNames(name, variants):
354
        ToStdout("%3s: %s", number, entry)
355
        choices.append(("%s" % number, entry, entry))
356
        number += 1
357

    
358
    choices.append(("x", "exit", "Exit gnt-instance reinstall"))
359
    selected = AskUser("Enter OS template number (or x to abort):",
360
                       choices)
361

    
362
    if selected == "exit":
363
      ToStderr("User aborted reinstall, exiting")
364
      return 1
365

    
366
    os_name = selected
367
    os_msg = "change the OS to '%s'" % selected
368
  else:
369
    os_name = opts.os
370
    if opts.os is not None:
371
      os_msg = "change the OS to '%s'" % os_name
372
    else:
373
      os_msg = "keep the same OS"
374

    
375
  # third, get confirmation: multi-reinstall requires --force-multi,
376
  # single-reinstall either --force or --force-multi (--force-multi is
377
  # a stronger --force)
378
  multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
379
  if multi_on:
380
    warn_msg = ("Note: this will remove *all* data for the"
381
                " below instances! It will %s.\n" % os_msg)
382
    if not (opts.force_multi or
383
            ConfirmOperation(inames, "instances", "reinstall", extra=warn_msg)):
384
      return 1
385
  else:
386
    if not (opts.force or opts.force_multi):
387
      usertext = ("This will reinstall the instance '%s' (and %s) which"
388
                  " removes all data. Continue?") % (inames[0], os_msg)
389
      if not AskUser(usertext):
390
        return 1
391

    
392
  jex = JobExecutor(verbose=multi_on, opts=opts)
393
  for instance_name in inames:
394
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
395
                                     os_type=os_name,
396
                                     force_variant=opts.force_variant,
397
                                     osparams=opts.osparams,
398
                                     osparams_private=opts.osparams_private,
399
                                     osparams_secret=opts.osparams_secret)
400
    jex.QueueJob(instance_name, op)
401

    
402
  results = jex.WaitOrShow(not opts.submit_only)
403

    
404
  if compat.all(map(compat.fst, results)):
405
    return constants.EXIT_SUCCESS
406
  else:
407
    return constants.EXIT_FAILURE
408

    
409

    
410
def RemoveInstance(opts, args):
411
  """Remove an instance.
412

413
  @param opts: the command line options selected by the user
414
  @type args: list
415
  @param args: should contain only one element, the name of
416
      the instance to be removed
417
  @rtype: int
418
  @return: the desired exit code
419

420
  """
421
  instance_name = args[0]
422
  force = opts.force
423
  cl = GetClient()
424

    
425
  if not force:
426
    _EnsureInstancesExist(cl, [instance_name])
427

    
428
    usertext = ("This will remove the volumes of the instance %s"
429
                " (including mirrors), thus removing all the data"
430
                " of the instance. Continue?") % instance_name
431
    if not AskUser(usertext):
432
      return 1
433

    
434
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
435
                                ignore_failures=opts.ignore_failures,
436
                                shutdown_timeout=opts.shutdown_timeout)
437
  SubmitOrSend(op, opts, cl=cl)
438
  return 0
439

    
440

    
441
def RenameInstance(opts, args):
442
  """Rename an instance.
443

444
  @param opts: the command line options selected by the user
445
  @type args: list
446
  @param args: should contain two elements, the old and the
447
      new instance names
448
  @rtype: int
449
  @return: the desired exit code
450

451
  """
452
  if not opts.name_check:
453
    if not AskUser("As you disabled the check of the DNS entry, please verify"
454
                   " that '%s' is a FQDN. Continue?" % args[1]):
455
      return 1
456

    
457
  op = opcodes.OpInstanceRename(instance_name=args[0],
458
                                new_name=args[1],
459
                                ip_check=opts.ip_check,
460
                                name_check=opts.name_check)
461
  result = SubmitOrSend(op, opts)
462

    
463
  if result:
464
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
465

    
466
  return 0
467

    
468

    
469
def ActivateDisks(opts, args):
470
  """Activate an instance's disks.
471

472
  This serves two purposes:
473
    - it allows (as long as the instance is not running)
474
      mounting the disks and modifying them from the node
475
    - it repairs inactive secondary drbds
476

477
  @param opts: the command line options selected by the user
478
  @type args: list
479
  @param args: should contain only one element, the instance name
480
  @rtype: int
481
  @return: the desired exit code
482

483
  """
484
  instance_name = args[0]
485
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
486
                                       ignore_size=opts.ignore_size,
487
                                       wait_for_sync=opts.wait_for_sync)
488
  disks_info = SubmitOrSend(op, opts)
489
  for host, iname, nname in disks_info:
490
    ToStdout("%s:%s:%s", host, iname, nname)
491
  return 0
492

    
493

    
494
def DeactivateDisks(opts, args):
495
  """Deactivate an instance's disks.
496

497
  This function takes the instance name, looks for its primary node
498
  and the tries to shutdown its block devices on that node.
499

500
  @param opts: the command line options selected by the user
501
  @type args: list
502
  @param args: should contain only one element, the instance name
503
  @rtype: int
504
  @return: the desired exit code
505

506
  """
507
  instance_name = args[0]
508
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
509
                                         force=opts.force)
510
  SubmitOrSend(op, opts)
511
  return 0
512

    
513

    
514
def RecreateDisks(opts, args):
515
  """Recreate an instance's disks.
516

517
  @param opts: the command line options selected by the user
518
  @type args: list
519
  @param args: should contain only one element, the instance name
520
  @rtype: int
521
  @return: the desired exit code
522

523
  """
524
  instance_name = args[0]
525

    
526
  disks = []
527

    
528
  if opts.disks:
529
    for didx, ddict in opts.disks:
530
      didx = int(didx)
531

    
532
      if not ht.TDict(ddict):
533
        msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
534
        raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
535

    
536
      if constants.IDISK_SIZE in ddict:
537
        try:
538
          ddict[constants.IDISK_SIZE] = \
539
            utils.ParseUnit(ddict[constants.IDISK_SIZE])
540
        except ValueError, err:
541
          raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
542
                                     (didx, err), errors.ECODE_INVAL)
543

    
544
      if constants.IDISK_SPINDLES in ddict:
545
        try:
546
          ddict[constants.IDISK_SPINDLES] = \
547
              int(ddict[constants.IDISK_SPINDLES])
548
        except ValueError, err:
549
          raise errors.OpPrereqError("Invalid spindles for disk %d: %s" %
550
                                     (didx, err), errors.ECODE_INVAL)
551

    
552
      disks.append((didx, ddict))
553

    
554
    # TODO: Verify modifyable parameters (already done in
555
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
556

    
557
  if opts.node:
558
    if opts.iallocator:
559
      msg = "At most one of either --nodes or --iallocator can be passed"
560
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
561
    pnode, snode = SplitNodeOption(opts.node)
562
    nodes = [pnode]
563
    if snode is not None:
564
      nodes.append(snode)
565
  else:
566
    nodes = []
567

    
568
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
569
                                       disks=disks, nodes=nodes,
570
                                       iallocator=opts.iallocator)
571
  SubmitOrSend(op, opts)
572

    
573
  return 0
574

    
575

    
576
def GrowDisk(opts, args):
577
  """Grow an instance's disks.
578

579
  @param opts: the command line options selected by the user
580
  @type args: list
581
  @param args: should contain three elements, the target instance name,
582
      the target disk id, and the target growth
583
  @rtype: int
584
  @return: the desired exit code
585

586
  """
587
  instance = args[0]
588
  disk = args[1]
589
  try:
590
    disk = int(disk)
591
  except (TypeError, ValueError), err:
592
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
593
                               errors.ECODE_INVAL)
594
  try:
595
    amount = utils.ParseUnit(args[2])
596
  except errors.UnitParseError:
597
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
598
                               errors.ECODE_INVAL)
599
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
600
                                  disk=disk, amount=amount,
601
                                  wait_for_sync=opts.wait_for_sync,
602
                                  absolute=opts.absolute)
603
  SubmitOrSend(op, opts)
604
  return 0
605

    
606

    
607
def _StartupInstance(name, opts):
608
  """Startup instances.
609

610
  This returns the opcode to start an instance, and its decorator will
611
  wrap this into a loop starting all desired instances.
612

613
  @param name: the name of the instance to act on
614
  @param opts: the command line options selected by the user
615
  @return: the opcode needed for the operation
616

617
  """
618
  op = opcodes.OpInstanceStartup(instance_name=name,
619
                                 force=opts.force,
620
                                 ignore_offline_nodes=opts.ignore_offline,
621
                                 no_remember=opts.no_remember,
622
                                 startup_paused=opts.startup_paused)
623
  # do not add these parameters to the opcode unless they're defined
624
  if opts.hvparams:
625
    op.hvparams = opts.hvparams
626
  if opts.beparams:
627
    op.beparams = opts.beparams
628
  return op
629

    
630

    
631
def _RebootInstance(name, opts):
632
  """Reboot instance(s).
633

634
  This returns the opcode to reboot an instance, and its decorator
635
  will wrap this into a loop rebooting all desired instances.
636

637
  @param name: the name of the instance to act on
638
  @param opts: the command line options selected by the user
639
  @return: the opcode needed for the operation
640

641
  """
642
  return opcodes.OpInstanceReboot(instance_name=name,
643
                                  reboot_type=opts.reboot_type,
644
                                  ignore_secondaries=opts.ignore_secondaries,
645
                                  shutdown_timeout=opts.shutdown_timeout)
646

    
647

    
648
def _ShutdownInstance(name, opts):
649
  """Shutdown an instance.
650

651
  This returns the opcode to shutdown an instance, and its decorator
652
  will wrap this into a loop shutting down all desired instances.
653

654
  @param name: the name of the instance to act on
655
  @param opts: the command line options selected by the user
656
  @return: the opcode needed for the operation
657

658
  """
659
  return opcodes.OpInstanceShutdown(instance_name=name,
660
                                    force=opts.force,
661
                                    timeout=opts.timeout,
662
                                    ignore_offline_nodes=opts.ignore_offline,
663
                                    no_remember=opts.no_remember)
664

    
665

    
666
def ReplaceDisks(opts, args):
667
  """Replace the disks of an instance
668

669
  @param opts: the command line options selected by the user
670
  @type args: list
671
  @param args: should contain only one element, the instance name
672
  @rtype: int
673
  @return: the desired exit code
674

675
  """
676
  new_2ndary = opts.dst_node
677
  iallocator = opts.iallocator
678
  if opts.disks is None:
679
    disks = []
680
  else:
681
    try:
682
      disks = [int(i) for i in opts.disks.split(",")]
683
    except (TypeError, ValueError), err:
684
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
685
                                 errors.ECODE_INVAL)
686
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
687
         new_2ndary is not None, iallocator is not None].count(True)
688
  if cnt != 1:
689
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
690
                               " options must be passed", errors.ECODE_INVAL)
691
  elif opts.on_primary:
692
    mode = constants.REPLACE_DISK_PRI
693
  elif opts.on_secondary:
694
    mode = constants.REPLACE_DISK_SEC
695
  elif opts.auto:
696
    mode = constants.REPLACE_DISK_AUTO
697
    if disks:
698
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
699
                                 " mode", errors.ECODE_INVAL)
700
  elif new_2ndary is not None or iallocator is not None:
701
    # replace secondary
702
    mode = constants.REPLACE_DISK_CHG
703

    
704
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
705
                                      remote_node=new_2ndary, mode=mode,
706
                                      iallocator=iallocator,
707
                                      early_release=opts.early_release,
708
                                      ignore_ipolicy=opts.ignore_ipolicy)
709
  SubmitOrSend(op, opts)
710
  return 0
711

    
712

    
713
def FailoverInstance(opts, args):
714
  """Failover an instance.
715

716
  The failover is done by shutting it down on its present node and
717
  starting it on the secondary.
718

719
  @param opts: the command line options selected by the user
720
  @type args: list
721
  @param args: should contain only one element, the instance name
722
  @rtype: int
723
  @return: the desired exit code
724

725
  """
726
  cl = GetClient()
727
  instance_name = args[0]
728
  force = opts.force
729
  iallocator = opts.iallocator
730
  target_node = opts.dst_node
731

    
732
  if iallocator and target_node:
733
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
734
                               " node (-n) but not both", errors.ECODE_INVAL)
735

    
736
  if not force:
737
    _EnsureInstancesExist(cl, [instance_name])
738

    
739
    usertext = ("Failover will happen to image %s."
740
                " This requires a shutdown of the instance. Continue?" %
741
                (instance_name,))
742
    if not AskUser(usertext):
743
      return 1
744

    
745
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
746
                                  ignore_consistency=opts.ignore_consistency,
747
                                  shutdown_timeout=opts.shutdown_timeout,
748
                                  iallocator=iallocator,
749
                                  target_node=target_node,
750
                                  ignore_ipolicy=opts.ignore_ipolicy)
751
  SubmitOrSend(op, opts, cl=cl)
752
  return 0
753

    
754

    
755
def MigrateInstance(opts, args):
756
  """Migrate an instance.
757

758
  The migrate is done without shutdown.
759

760
  @param opts: the command line options selected by the user
761
  @type args: list
762
  @param args: should contain only one element, the instance name
763
  @rtype: int
764
  @return: the desired exit code
765

766
  """
767
  cl = GetClient()
768
  instance_name = args[0]
769
  force = opts.force
770
  iallocator = opts.iallocator
771
  target_node = opts.dst_node
772

    
773
  if iallocator and target_node:
774
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
775
                               " node (-n) but not both", errors.ECODE_INVAL)
776

    
777
  if not force:
778
    _EnsureInstancesExist(cl, [instance_name])
779

    
780
    if opts.cleanup:
781
      usertext = ("Instance %s will be recovered from a failed migration."
782
                  " Note that the migration procedure (including cleanup)" %
783
                  (instance_name,))
784
    else:
785
      usertext = ("Instance %s will be migrated. Note that migration" %
786
                  (instance_name,))
787
    usertext += (" might impact the instance if anything goes wrong"
788
                 " (e.g. due to bugs in the hypervisor). Continue?")
789
    if not AskUser(usertext):
790
      return 1
791

    
792
  # this should be removed once --non-live is deprecated
793
  if not opts.live and opts.migration_mode is not None:
794
    raise errors.OpPrereqError("Only one of the --non-live and "
795
                               "--migration-mode options can be passed",
796
                               errors.ECODE_INVAL)
797
  if not opts.live: # --non-live passed
798
    mode = constants.HT_MIGRATION_NONLIVE
799
  else:
800
    mode = opts.migration_mode
801

    
802
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
803
                                 cleanup=opts.cleanup, iallocator=iallocator,
804
                                 target_node=target_node,
805
                                 allow_failover=opts.allow_failover,
806
                                 allow_runtime_changes=opts.allow_runtime_chgs,
807
                                 ignore_ipolicy=opts.ignore_ipolicy)
808
  SubmitOrSend(op, cl=cl, opts=opts)
809
  return 0
810

    
811

    
812
def MoveInstance(opts, args):
813
  """Move an instance.
814

815
  @param opts: the command line options selected by the user
816
  @type args: list
817
  @param args: should contain only one element, the instance name
818
  @rtype: int
819
  @return: the desired exit code
820

821
  """
822
  cl = GetClient()
823
  instance_name = args[0]
824
  force = opts.force
825

    
826
  if not force:
827
    usertext = ("Instance %s will be moved."
828
                " This requires a shutdown of the instance. Continue?" %
829
                (instance_name,))
830
    if not AskUser(usertext):
831
      return 1
832

    
833
  op = opcodes.OpInstanceMove(instance_name=instance_name,
834
                              target_node=opts.node,
835
                              compress=opts.compress,
836
                              shutdown_timeout=opts.shutdown_timeout,
837
                              ignore_consistency=opts.ignore_consistency,
838
                              ignore_ipolicy=opts.ignore_ipolicy)
839
  SubmitOrSend(op, opts, cl=cl)
840
  return 0
841

    
842

    
843
def ConnectToInstanceConsole(opts, args):
844
  """Connect to the console of an instance.
845

846
  @param opts: the command line options selected by the user
847
  @type args: list
848
  @param args: should contain only one element, the instance name
849
  @rtype: int
850
  @return: the desired exit code
851

852
  """
853
  instance_name = args[0]
854

    
855
  cl = GetClient()
856
  try:
857
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
858
    ((console_data, oper_state), ) = \
859
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
860
  finally:
861
    # Ensure client connection is closed while external commands are run
862
    cl.Close()
863

    
864
  del cl
865

    
866
  if not console_data:
867
    if oper_state:
868
      # Instance is running
869
      raise errors.OpExecError("Console information for instance %s is"
870
                               " unavailable" % instance_name)
871
    else:
872
      raise errors.OpExecError("Instance %s is not running, can't get console" %
873
                               instance_name)
874

    
875
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
876
                    opts.show_command, cluster_name)
877

    
878

    
879
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
880
               _runcmd_fn=utils.RunCmd):
881
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
882

883
  @type console: L{objects.InstanceConsole}
884
  @param console: Console object
885
  @type show_command: bool
886
  @param show_command: Whether to just display commands
887
  @type cluster_name: string
888
  @param cluster_name: Cluster name as retrieved from master daemon
889

890
  """
891
  assert console.Validate()
892

    
893
  if console.kind == constants.CONS_MESSAGE:
894
    feedback_fn(console.message)
895
  elif console.kind == constants.CONS_VNC:
896
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
897
                " URL <vnc://%s:%s/>",
898
                console.instance, console.host, console.port,
899
                console.display, console.host, console.port)
900
  elif console.kind == constants.CONS_SPICE:
901
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
902
                console.host, console.port)
903
  elif console.kind == constants.CONS_SSH:
904
    # Convert to string if not already one
905
    if isinstance(console.command, basestring):
906
      cmd = console.command
907
    else:
908
      cmd = utils.ShellQuoteArgs(console.command)
909

    
910
    srun = ssh.SshRunner(cluster_name=cluster_name)
911
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
912
                            port=console.port,
913
                            batch=True, quiet=False, tty=True)
914

    
915
    if show_command:
916
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
917
    else:
918
      result = _runcmd_fn(ssh_cmd, interactive=True)
919
      if result.failed:
920
        logging.error("Console command \"%s\" failed with reason '%s' and"
921
                      " output %r", result.cmd, result.fail_reason,
922
                      result.output)
923
        raise errors.OpExecError("Connection to console of instance %s failed,"
924
                                 " please check cluster configuration" %
925
                                 console.instance)
926
  else:
927
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
928

    
929
  return constants.EXIT_SUCCESS
930

    
931

    
932
def _FormatDiskDetails(dev_type, dev, roman):
933
  """Formats the logical_id of a disk.
934

935
  """
936
  if dev_type == constants.DT_DRBD8:
937
    drbd_info = dev["drbd_info"]
938
    data = [
939
      ("nodeA", "%s, minor=%s" %
940
                (drbd_info["primary_node"],
941
                 compat.TryToRoman(drbd_info["primary_minor"],
942
                                   convert=roman))),
943
      ("nodeB", "%s, minor=%s" %
944
                (drbd_info["secondary_node"],
945
                 compat.TryToRoman(drbd_info["secondary_minor"],
946
                                   convert=roman))),
947
      ("port", str(compat.TryToRoman(drbd_info["port"], convert=roman))),
948
      ("auth key", str(drbd_info["secret"])),
949
      ]
950
  elif dev_type == constants.DT_PLAIN:
951
    vg_name, lv_name = dev["logical_id"]
952
    data = ["%s/%s" % (vg_name, lv_name)]
953
  else:
954
    data = [str(dev["logical_id"])]
955

    
956
  return data
957

    
958

    
959
def _FormatBlockDevInfo(idx, top_level, dev, roman):
960
  """Show block device information.
961

962
  This is only used by L{ShowInstanceConfig}, but it's too big to be
963
  left for an inline definition.
964

965
  @type idx: int
966
  @param idx: the index of the current disk
967
  @type top_level: boolean
968
  @param top_level: if this a top-level disk?
969
  @type dev: dict
970
  @param dev: dictionary with disk information
971
  @type roman: boolean
972
  @param roman: whether to try to use roman integers
973
  @return: a list of either strings, tuples or lists
974
      (which should be formatted at a higher indent level)
975

976
  """
977
  def helper(dtype, status):
978
    """Format one line for physical device status.
979

980
    @type dtype: str
981
    @param dtype: a constant from the L{constants.DTS_BLOCK} set
982
    @type status: tuple
983
    @param status: a tuple as returned from L{backend.FindBlockDevice}
984
    @return: the string representing the status
985

986
    """
987
    if not status:
988
      return "not active"
989
    txt = ""
990
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
991
    if major is None:
992
      major_string = "N/A"
993
    else:
994
      major_string = str(compat.TryToRoman(major, convert=roman))
995

    
996
    if minor is None:
997
      minor_string = "N/A"
998
    else:
999
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1000

    
1001
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
1002
    if dtype in (constants.DT_DRBD8, ):
1003
      if syncp is not None:
1004
        sync_text = "*RECOVERING* %5.2f%%," % syncp
1005
        if estt:
1006
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1007
        else:
1008
          sync_text += " ETA unknown"
1009
      else:
1010
        sync_text = "in sync"
1011
      if degr:
1012
        degr_text = "*DEGRADED*"
1013
      else:
1014
        degr_text = "ok"
1015
      if ldisk_status == constants.LDS_FAULTY:
1016
        ldisk_text = " *MISSING DISK*"
1017
      elif ldisk_status == constants.LDS_UNKNOWN:
1018
        ldisk_text = " *UNCERTAIN STATE*"
1019
      else:
1020
        ldisk_text = ""
1021
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1022
    elif dtype == constants.DT_PLAIN:
1023
      if ldisk_status == constants.LDS_FAULTY:
1024
        ldisk_text = " *FAILED* (failed drive?)"
1025
      else:
1026
        ldisk_text = ""
1027
      txt += ldisk_text
1028
    return txt
1029

    
1030
  # the header
1031
  if top_level:
1032
    if dev["iv_name"] is not None:
1033
      txt = dev["iv_name"]
1034
    else:
1035
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1036
  else:
1037
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1038
  if isinstance(dev["size"], int):
1039
    nice_size = utils.FormatUnit(dev["size"], "h")
1040
  else:
1041
    nice_size = str(dev["size"])
1042
  data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1043
  if top_level:
1044
    if dev["spindles"] is not None:
1045
      data.append(("spindles", dev["spindles"]))
1046
    data.append(("access mode", dev["mode"]))
1047
  if dev["logical_id"] is not None:
1048
    try:
1049
      l_id = _FormatDiskDetails(dev["dev_type"], dev, roman)
1050
    except ValueError:
1051
      l_id = [str(dev["logical_id"])]
1052
    if len(l_id) == 1:
1053
      data.append(("logical_id", l_id[0]))
1054
    else:
1055
      data.extend(l_id)
1056

    
1057
  if dev["pstatus"]:
1058
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1059

    
1060
  if dev["sstatus"]:
1061
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1062

    
1063
  data.append(("name", dev["name"]))
1064
  data.append(("UUID", dev["uuid"]))
1065

    
1066
  if dev["children"]:
1067
    data.append(("child devices", [
1068
      _FormatBlockDevInfo(c_idx, False, child, roman)
1069
      for c_idx, child in enumerate(dev["children"])
1070
      ]))
1071
  return data
1072

    
1073

    
1074
def _FormatInstanceNicInfo(idx, nic):
1075
  """Helper function for L{_FormatInstanceInfo()}"""
1076
  (name, uuid, ip, mac, mode, link, vlan, _, netinfo) = nic
1077
  network_name = None
1078
  if netinfo:
1079
    network_name = netinfo["name"]
1080
  return [
1081
    ("nic/%d" % idx, ""),
1082
    ("MAC", str(mac)),
1083
    ("IP", str(ip)),
1084
    ("mode", str(mode)),
1085
    ("link", str(link)),
1086
    ("vlan", str(vlan)),
1087
    ("network", str(network_name)),
1088
    ("UUID", str(uuid)),
1089
    ("name", str(name)),
1090
    ]
1091

    
1092

    
1093
def _FormatInstanceNodesInfo(instance):
1094
  """Helper function for L{_FormatInstanceInfo()}"""
1095
  pgroup = ("%s (UUID %s)" %
1096
            (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1097
  secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1098
                          (name, group_name, group_uuid))
1099
                         for (name, group_name, group_uuid) in
1100
                           zip(instance["snodes"],
1101
                               instance["snodes_group_names"],
1102
                               instance["snodes_group_uuids"]))
1103
  return [
1104
    [
1105
      ("primary", instance["pnode"]),
1106
      ("group", pgroup),
1107
      ],
1108
    [("secondaries", secs)],
1109
    ]
1110

    
1111

    
1112
def _GetVncConsoleInfo(instance):
1113
  """Helper function for L{_FormatInstanceInfo()}"""
1114
  vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1115
                                               None)
1116
  if vnc_bind_address:
1117
    port = instance["network_port"]
1118
    display = int(port) - constants.VNC_BASE_PORT
1119
    if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1120
      vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1121
                                                 port,
1122
                                                 display)
1123
    elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1124
      vnc_console_port = ("%s:%s (node %s) (display %s)" %
1125
                           (vnc_bind_address, port,
1126
                            instance["pnode"], display))
1127
    else:
1128
      # vnc bind address is a file
1129
      vnc_console_port = "%s:%s" % (instance["pnode"],
1130
                                    vnc_bind_address)
1131
    ret = "vnc to %s" % vnc_console_port
1132
  else:
1133
    ret = None
1134
  return ret
1135

    
1136

    
1137
def _FormatInstanceInfo(instance, roman_integers):
1138
  """Format instance information for L{cli.PrintGenericInfo()}"""
1139
  istate = "configured to be %s" % instance["config_state"]
1140
  if instance["run_state"]:
1141
    istate += ", actual state is %s" % instance["run_state"]
1142
  info = [
1143
    ("Instance name", instance["name"]),
1144
    ("UUID", instance["uuid"]),
1145
    ("Serial number",
1146
     str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1147
    ("Creation time", utils.FormatTime(instance["ctime"])),
1148
    ("Modification time", utils.FormatTime(instance["mtime"])),
1149
    ("State", istate),
1150
    ("Nodes", _FormatInstanceNodesInfo(instance)),
1151
    ("Operating system", instance["os"]),
1152
    ("Operating system parameters",
1153
     FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1154
    ]
1155

    
1156
  if "network_port" in instance:
1157
    info.append(("Allocated network port",
1158
                 str(compat.TryToRoman(instance["network_port"],
1159
                                       convert=roman_integers))))
1160
  info.append(("Hypervisor", instance["hypervisor"]))
1161
  console = _GetVncConsoleInfo(instance)
1162
  if console:
1163
    info.append(("console connection", console))
1164
  # deprecated "memory" value, kept for one version for compatibility
1165
  # TODO(ganeti 2.7) remove.
1166
  be_actual = copy.deepcopy(instance["be_actual"])
1167
  be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1168
  info.extend([
1169
    ("Hypervisor parameters",
1170
     FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1171
    ("Back-end parameters",
1172
     FormatParamsDictInfo(instance["be_instance"], be_actual)),
1173
    ("NICs", [
1174
      _FormatInstanceNicInfo(idx, nic)
1175
      for (idx, nic) in enumerate(instance["nics"])
1176
      ]),
1177
    ("Disk template", instance["disk_template"]),
1178
    ("Disks", [
1179
      _FormatBlockDevInfo(idx, True, device, roman_integers)
1180
      for (idx, device) in enumerate(instance["disks"])
1181
      ]),
1182
    ])
1183
  return info
1184

    
1185

    
1186
def ShowInstanceConfig(opts, args):
1187
  """Compute instance run-time status.
1188

1189
  @param opts: the command line options selected by the user
1190
  @type args: list
1191
  @param args: either an empty list, and then we query all
1192
      instances, or should contain a list of instance names
1193
  @rtype: int
1194
  @return: the desired exit code
1195

1196
  """
1197
  if not args and not opts.show_all:
1198
    ToStderr("No instance selected."
1199
             " Please pass in --all if you want to query all instances.\n"
1200
             "Note that this can take a long time on a big cluster.")
1201
    return 1
1202
  elif args and opts.show_all:
1203
    ToStderr("Cannot use --all if you specify instance names.")
1204
    return 1
1205

    
1206
  retcode = 0
1207
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1208
                                   use_locking=not opts.static)
1209
  result = SubmitOpCode(op, opts=opts)
1210
  if not result:
1211
    ToStdout("No instances.")
1212
    return 1
1213

    
1214
  PrintGenericInfo([
1215
    _FormatInstanceInfo(instance, opts.roman_integers)
1216
    for instance in result.values()
1217
    ])
1218
  return retcode
1219

    
1220

    
1221
def _ConvertNicDiskModifications(mods):
1222
  """Converts NIC/disk modifications from CLI to opcode.
1223

1224
  When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
1225
  disks at arbitrary indices, its parameter format changed. This function
1226
  converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
1227
  newer format and adds support for new-style requests (e.g. "--new 4:add").
1228

1229
  @type mods: list of tuples
1230
  @param mods: Modifications as given by command line parser
1231
  @rtype: list of tuples
1232
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1233

1234
  """
1235
  result = []
1236

    
1237
  for (identifier, params) in mods:
1238
    if identifier == constants.DDM_ADD:
1239
      # Add item as last item (legacy interface)
1240
      action = constants.DDM_ADD
1241
      identifier = -1
1242
    elif identifier == constants.DDM_REMOVE:
1243
      # Remove last item (legacy interface)
1244
      action = constants.DDM_REMOVE
1245
      identifier = -1
1246
    else:
1247
      # Modifications and adding/removing at arbitrary indices
1248
      add = params.pop(constants.DDM_ADD, _MISSING)
1249
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1250
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1251

    
1252
      if modify is _MISSING:
1253
        if not (add is _MISSING or remove is _MISSING):
1254
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1255
                                     errors.ECODE_INVAL)
1256
        elif add is not _MISSING:
1257
          action = constants.DDM_ADD
1258
        elif remove is not _MISSING:
1259
          action = constants.DDM_REMOVE
1260
        else:
1261
          action = constants.DDM_MODIFY
1262

    
1263
      elif add is _MISSING and remove is _MISSING:
1264
        action = constants.DDM_MODIFY
1265
      else:
1266
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1267
                                   " same time", errors.ECODE_INVAL)
1268

    
1269
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1270

    
1271
    if action == constants.DDM_REMOVE and params:
1272
      raise errors.OpPrereqError("Not accepting parameters on removal",
1273
                                 errors.ECODE_INVAL)
1274

    
1275
    result.append((action, identifier, params))
1276

    
1277
  return result
1278

    
1279

    
1280
def _ParseDiskSizes(mods):
1281
  """Parses disk sizes in parameters.
1282

1283
  """
1284
  for (action, _, params) in mods:
1285
    if params and constants.IDISK_SPINDLES in params:
1286
      params[constants.IDISK_SPINDLES] = \
1287
          int(params[constants.IDISK_SPINDLES])
1288
    if params and constants.IDISK_SIZE in params:
1289
      params[constants.IDISK_SIZE] = \
1290
        utils.ParseUnit(params[constants.IDISK_SIZE])
1291
    elif action == constants.DDM_ADD:
1292
      raise errors.OpPrereqError("Missing required parameter 'size'",
1293
                                 errors.ECODE_INVAL)
1294

    
1295
  return mods
1296

    
1297

    
1298
def SetInstanceParams(opts, args):
1299
  """Modifies an instance.
1300

1301
  All parameters take effect only at the next restart of the instance.
1302

1303
  @param opts: the command line options selected by the user
1304
  @type args: list
1305
  @param args: should contain only one element, the instance name
1306
  @rtype: int
1307
  @return: the desired exit code
1308

1309
  """
1310
  if not (opts.nics or opts.disks or opts.disk_template or opts.hvparams or
1311
          opts.beparams or opts.os or opts.osparams or opts.osparams_private
1312
          or opts.offline_inst or opts.online_inst or opts.runtime_mem or
1313
          opts.new_primary_node or opts.instance_communication is not None):
1314
    ToStderr("Please give at least one of the parameters.")
1315
    return 1
1316

    
1317
  for param in opts.beparams:
1318
    if isinstance(opts.beparams[param], basestring):
1319
      if opts.beparams[param].lower() == "default":
1320
        opts.beparams[param] = constants.VALUE_DEFAULT
1321

    
1322
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1323
                      allowed_values=[constants.VALUE_DEFAULT])
1324

    
1325
  for param in opts.hvparams:
1326
    if isinstance(opts.hvparams[param], basestring):
1327
      if opts.hvparams[param].lower() == "default":
1328
        opts.hvparams[param] = constants.VALUE_DEFAULT
1329

    
1330
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1331
                      allowed_values=[constants.VALUE_DEFAULT])
1332
  FixHvParams(opts.hvparams)
1333

    
1334
  nics = _ConvertNicDiskModifications(opts.nics)
1335
  for action, _, __ in nics:
1336
    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
1337
      usertext = ("You are about to hot-modify a NIC. This will be done"
1338
                  " by removing the exisiting and then adding a new one."
1339
                  " Network connection might be lost. Continue?")
1340
      if not AskUser(usertext):
1341
        return 1
1342

    
1343
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1344

    
1345
  if (opts.disk_template and
1346
      opts.disk_template in constants.DTS_INT_MIRROR and
1347
      not opts.node):
1348
    ToStderr("Changing the disk template to a mirrored one requires"
1349
             " specifying a secondary node")
1350
    return 1
1351

    
1352
  if opts.offline_inst:
1353
    offline = True
1354
  elif opts.online_inst:
1355
    offline = False
1356
  else:
1357
    offline = None
1358

    
1359
  instance_comm = opts.instance_communication
1360

    
1361
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1362
                                   nics=nics,
1363
                                   disks=disks,
1364
                                   hotplug=opts.hotplug,
1365
                                   hotplug_if_possible=opts.hotplug_if_possible,
1366
                                   disk_template=opts.disk_template,
1367
                                   remote_node=opts.node,
1368
                                   pnode=opts.new_primary_node,
1369
                                   hvparams=opts.hvparams,
1370
                                   beparams=opts.beparams,
1371
                                   runtime_mem=opts.runtime_mem,
1372
                                   os_name=opts.os,
1373
                                   osparams=opts.osparams,
1374
                                   osparams_private=opts.osparams_private,
1375
                                   force_variant=opts.force_variant,
1376
                                   force=opts.force,
1377
                                   wait_for_sync=opts.wait_for_sync,
1378
                                   offline=offline,
1379
                                   conflicts_check=opts.conflicts_check,
1380
                                   ignore_ipolicy=opts.ignore_ipolicy,
1381
                                   instance_communication=instance_comm)
1382

    
1383
  # even if here we process the result, we allow submit only
1384
  result = SubmitOrSend(op, opts)
1385

    
1386
  if result:
1387
    ToStdout("Modified instance %s", args[0])
1388
    for param, data in result:
1389
      ToStdout(" - %-5s -> %s", param, data)
1390
    ToStdout("Please don't forget that most parameters take effect"
1391
             " only at the next (re)start of the instance initiated by"
1392
             " ganeti; restarting from within the instance will"
1393
             " not be enough.")
1394
  return 0
1395

    
1396

    
1397
def ChangeGroup(opts, args):
1398
  """Moves an instance to another group.
1399

1400
  """
1401
  (instance_name, ) = args
1402

    
1403
  cl = GetClient()
1404

    
1405
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1406
                                     iallocator=opts.iallocator,
1407
                                     target_groups=opts.to,
1408
                                     early_release=opts.early_release)
1409
  result = SubmitOrSend(op, opts, cl=cl)
1410

    
1411
  # Keep track of submitted jobs
1412
  jex = JobExecutor(cl=cl, opts=opts)
1413

    
1414
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1415
    jex.AddJobId(None, status, job_id)
1416

    
1417
  results = jex.GetResults()
1418
  bad_cnt = len([row for row in results if not row[0]])
1419
  if bad_cnt == 0:
1420
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1421
    rcode = constants.EXIT_SUCCESS
1422
  else:
1423
    ToStdout("There were %s errors while changing group of instance '%s'.",
1424
             bad_cnt, instance_name)
1425
    rcode = constants.EXIT_FAILURE
1426

    
1427
  return rcode
1428

    
1429

    
1430
# multi-instance selection options
1431
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1432
                           help="Do not ask for confirmation when more than"
1433
                           " one instance is affected",
1434
                           action="store_true", default=False)
1435

    
1436
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1437
                            help="Filter by nodes (primary only)",
1438
                            const=_EXPAND_NODES_PRI, action="store_const")
1439

    
1440
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1441
                            help="Filter by nodes (secondary only)",
1442
                            const=_EXPAND_NODES_SEC, action="store_const")
1443

    
1444
m_node_opt = cli_option("--node", dest="multi_mode",
1445
                        help="Filter by nodes (primary and secondary)",
1446
                        const=_EXPAND_NODES_BOTH, action="store_const")
1447

    
1448
m_clust_opt = cli_option("--all", dest="multi_mode",
1449
                         help="Select all instances in the cluster",
1450
                         const=_EXPAND_CLUSTER, action="store_const")
1451

    
1452
m_inst_opt = cli_option("--instance", dest="multi_mode",
1453
                        help="Filter by instance name [default]",
1454
                        const=_EXPAND_INSTANCES, action="store_const")
1455

    
1456
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1457
                             help="Filter by node tag",
1458
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1459
                             action="store_const")
1460

    
1461
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1462
                                 help="Filter by primary node tag",
1463
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1464
                                 action="store_const")
1465

    
1466
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1467
                                 help="Filter by secondary node tag",
1468
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1469
                                 action="store_const")
1470

    
1471
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1472
                             help="Filter by instance tag",
1473
                             const=_EXPAND_INSTANCES_BY_TAGS,
1474
                             action="store_const")
1475

    
1476
# this is defined separately due to readability only
1477
add_opts = [
1478
  NOSTART_OPT,
1479
  OS_OPT,
1480
  FORCE_VARIANT_OPT,
1481
  NO_INSTALL_OPT,
1482
  IGNORE_IPOLICY_OPT,
1483
  INSTANCE_COMMUNICATION_OPT,
1484
  ]
1485

    
1486
commands = {
1487
  "add": (
1488
    AddInstance, [ArgHost(min=1, max=1)],
1489
    COMMON_CREATE_OPTS + add_opts,
1490
    "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1491
    "Creates and adds a new instance to the cluster"),
1492
  "batch-create": (
1493
    BatchCreate, [ArgFile(min=1, max=1)],
1494
    [DRY_RUN_OPT, PRIORITY_OPT, IALLOCATOR_OPT] + SUBMIT_OPTS,
1495
    "<instances.json>",
1496
    "Create a bunch of instances based on specs in the file."),
1497
  "console": (
1498
    ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1499
    [SHOWCMD_OPT, PRIORITY_OPT],
1500
    "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1501
  "failover": (
1502
    FailoverInstance, ARGS_ONE_INSTANCE,
1503
    [FORCE_OPT, IGNORE_CONSIST_OPT] + SUBMIT_OPTS +
1504
    [SHUTDOWN_TIMEOUT_OPT,
1505
     DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1506
     IGNORE_IPOLICY_OPT, CLEANUP_OPT],
1507
    "[-f] <instance>", "Stops the instance, changes its primary node and"
1508
    " (if it was originally running) starts it on the new node"
1509
    " (the secondary for mirrored instances or any node"
1510
    " for shared storage)."),
1511
  "migrate": (
1512
    MigrateInstance, ARGS_ONE_INSTANCE,
1513
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1514
     PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1515
     IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
1516
    "[-f] <instance>", "Migrate instance to its secondary node"
1517
    " (only for mirrored instances)"),
1518
  "move": (
1519
    MoveInstance, ARGS_ONE_INSTANCE,
1520
    [FORCE_OPT] + SUBMIT_OPTS +
1521
    [SINGLE_NODE_OPT, COMPRESS_OPT,
1522
     SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT,
1523
     IGNORE_IPOLICY_OPT],
1524
    "[-f] <instance>", "Move instance to an arbitrary node"
1525
    " (only for instances of type file and lv)"),
1526
  "info": (
1527
    ShowInstanceConfig, ARGS_MANY_INSTANCES,
1528
    [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1529
    "[-s] {--all | <instance>...}",
1530
    "Show information on the specified instance(s)"),
1531
  "list": (
1532
    ListInstances, ARGS_MANY_INSTANCES,
1533
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1534
     FORCE_FILTER_OPT],
1535
    "[<instance>...]",
1536
    "Lists the instances and their status. The available fields can be shown"
1537
    " using the \"list-fields\" command (see the man page for details)."
1538
    " The default field list is (in order): %s." %
1539
    utils.CommaJoin(_LIST_DEF_FIELDS),
1540
    ),
1541
  "list-fields": (
1542
    ListInstanceFields, [ArgUnknown()],
1543
    [NOHDR_OPT, SEP_OPT],
1544
    "[fields...]",
1545
    "Lists all available fields for instances"),
1546
  "reinstall": (
1547
    ReinstallInstance, [ArgInstance()],
1548
    [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1549
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1550
     m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT]
1551
    + SUBMIT_OPTS + [DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT,
1552
                     OSPARAMS_PRIVATE_OPT, OSPARAMS_SECRET_OPT],
1553
    "[-f] <instance>", "Reinstall a stopped instance"),
1554
  "remove": (
1555
    RemoveInstance, ARGS_ONE_INSTANCE,
1556
    [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT] + SUBMIT_OPTS
1557
    + [DRY_RUN_OPT, PRIORITY_OPT],
1558
    "[-f] <instance>", "Shuts down the instance and removes it"),
1559
  "rename": (
1560
    RenameInstance,
1561
    [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1562
    [NOIPCHECK_OPT, NONAMECHECK_OPT] + SUBMIT_OPTS
1563
    + [DRY_RUN_OPT, PRIORITY_OPT],
1564
    "<instance> <new_name>", "Rename the instance"),
1565
  "replace-disks": (
1566
    ReplaceDisks, ARGS_ONE_INSTANCE,
1567
    [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1568
     NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT] + SUBMIT_OPTS
1569
    + [DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1570
    "[-s|-p|-a|-n NODE|-I NAME] <instance>",
1571
    "Replaces disks for the instance"),
1572
  "modify": (
1573
    SetInstanceParams, ARGS_ONE_INSTANCE,
1574
    [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT] + SUBMIT_OPTS +
1575
    [DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1576
     OSPARAMS_OPT, OSPARAMS_PRIVATE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT,
1577
     OFFLINE_INST_OPT, ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
1578
     NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT, HOTPLUG_OPT,
1579
     HOTPLUG_IF_POSSIBLE_OPT, INSTANCE_COMMUNICATION_OPT],
1580
    "<instance>", "Alters the parameters of an instance"),
1581
  "shutdown": (
1582
    GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1583
    [FORCE_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1584
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1585
     m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT] + SUBMIT_OPTS
1586
    + [DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1587
    "<instance>", "Stops an instance"),
1588
  "startup": (
1589
    GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1590
    [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1591
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1592
     m_inst_tags_opt, m_clust_opt, m_inst_opt] + SUBMIT_OPTS +
1593
    [HVOPTS_OPT,
1594
     BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1595
     NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1596
    "<instance>", "Starts an instance"),
1597
  "reboot": (
1598
    GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1599
    [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1600
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt] + SUBMIT_OPTS +
1601
    [m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1602
     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1603
    "<instance>", "Reboots an instance"),
1604
  "activate-disks": (
1605
    ActivateDisks, ARGS_ONE_INSTANCE,
1606
    SUBMIT_OPTS + [IGNORE_SIZE_OPT, PRIORITY_OPT, WFSYNC_OPT],
1607
    "<instance>", "Activate an instance's disks"),
1608
  "deactivate-disks": (
1609
    DeactivateDisks, ARGS_ONE_INSTANCE,
1610
    [FORCE_OPT] + SUBMIT_OPTS + [DRY_RUN_OPT, PRIORITY_OPT],
1611
    "[-f] <instance>", "Deactivate an instance's disks"),
1612
  "recreate-disks": (
1613
    RecreateDisks, ARGS_ONE_INSTANCE,
1614
    SUBMIT_OPTS +
1615
    [DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
1616
     IALLOCATOR_OPT],
1617
    "<instance>", "Recreate an instance's disks"),
1618
  "grow-disk": (
1619
    GrowDisk,
1620
    [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1621
     ArgUnknown(min=1, max=1)],
1622
    SUBMIT_OPTS + [NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
1623
    "<instance> <disk> <size>", "Grow an instance's disk"),
1624
  "change-group": (
1625
    ChangeGroup, ARGS_ONE_INSTANCE,
1626
    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT]
1627
    + SUBMIT_OPTS,
1628
    "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1629
  "list-tags": (
1630
    ListTags, ARGS_ONE_INSTANCE, [],
1631
    "<instance_name>", "List the tags of the given instance"),
1632
  "add-tags": (
1633
    AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1634
    [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1635
    "<instance_name> tag...", "Add tags to the given instance"),
1636
  "remove-tags": (
1637
    RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1638
    [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1639
    "<instance_name> tag...", "Remove tags from given instance"),
1640
  }
1641

    
1642
#: dictionary with aliases for commands
1643
aliases = {
1644
  "start": "startup",
1645
  "stop": "shutdown",
1646
  "show": "info",
1647
  }
1648

    
1649

    
1650
def Main():
1651
  return GenericMain(commands, aliases=aliases,
1652
                     override={"tag_type": constants.TAG_INSTANCE},
1653
                     env_override=_ENV_OVERRIDE)