Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 9e448d7f

History | View | Annotate | Download (56.7 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

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

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

    
44

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

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

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

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

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

    
71

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

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

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

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

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

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

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

    
147
  return inames
148

    
149

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

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

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

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

    
171

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

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

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

    
205

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

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

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

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

    
226
  cl = GetClient(query=True)
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 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
    jex.QueueJob(instance_name, op)
399

    
400
  results = jex.WaitOrShow(not opts.submit_only)
401

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

    
407

    
408
def RemoveInstance(opts, args):
409
  """Remove an instance.
410

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

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

    
423
  if not force:
424
    _EnsureInstancesExist(cl, [instance_name])
425

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

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

    
438

    
439
def RenameInstance(opts, args):
440
  """Rename an instance.
441

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

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

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

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

    
464
  return 0
465

    
466

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

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

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

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

    
491

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

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

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

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

    
511

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

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

521
  """
522
  instance_name = args[0]
523

    
524
  disks = []
525

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

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

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

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

    
550
      disks.append((didx, ddict))
551

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

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

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

    
571
  return 0
572

    
573

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

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

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

    
604

    
605
def _StartupInstance(name, opts):
606
  """Startup instances.
607

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

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

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

    
628

    
629
def _RebootInstance(name, opts):
630
  """Reboot instance(s).
631

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

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

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

    
645

    
646
def _ShutdownInstance(name, opts):
647
  """Shutdown an instance.
648

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

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

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

    
663

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

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

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

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

    
710

    
711
def FailoverInstance(opts, args):
712
  """Failover an instance.
713

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

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

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

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

    
734
  if not force:
735
    _EnsureInstancesExist(cl, [instance_name])
736

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

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

    
752

    
753
def MigrateInstance(opts, args):
754
  """Migrate an instance.
755

756
  The migrate is done without shutdown.
757

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

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

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

    
775
  if not force:
776
    _EnsureInstancesExist(cl, [instance_name])
777

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

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

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

    
809

    
810
def MoveInstance(opts, args):
811
  """Move an instance.
812

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

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

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

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

    
840

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

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

850
  """
851
  instance_name = args[0]
852

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

    
862
  del cl
863

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

    
873
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
874
                    opts.show_command, cluster_name)
875

    
876

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

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

888
  """
889
  assert console.Validate()
890

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

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

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

    
927
  return constants.EXIT_SUCCESS
928

    
929

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

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

    
954
  return data
955

    
956

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

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

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

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

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

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

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

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

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

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

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

    
1061
  data.append(("name", dev["name"]))
1062
  data.append(("UUID", dev["uuid"]))
1063

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

    
1071

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

    
1090

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

    
1109

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

    
1134

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

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

    
1183

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

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

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

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

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

    
1218

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

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

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

1232
  """
1233
  result = []
1234

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

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

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

    
1267
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1268

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

    
1273
    result.append((action, identifier, params))
1274

    
1275
  return result
1276

    
1277

    
1278
def _ParseDiskSizes(mods):
1279
  """Parses disk sizes in parameters.
1280

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

    
1290
  return mods
1291

    
1292

    
1293
def SetInstanceParams(opts, args):
1294
  """Modifies an instance.
1295

1296
  All parameters take effect only at the next restart of the instance.
1297

1298
  @param opts: the command line options selected by the user
1299
  @type args: list
1300
  @param args: should contain only one element, the instance name
1301
  @rtype: int
1302
  @return: the desired exit code
1303

1304
  """
1305
  if not (opts.nics or opts.disks or opts.disk_template or
1306
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1307
          opts.offline_inst or opts.online_inst or opts.runtime_mem or
1308
          opts.new_primary_node):
1309
    ToStderr("Please give at least one of the parameters.")
1310
    return 1
1311

    
1312
  for param in opts.beparams:
1313
    if isinstance(opts.beparams[param], basestring):
1314
      if opts.beparams[param].lower() == "default":
1315
        opts.beparams[param] = constants.VALUE_DEFAULT
1316

    
1317
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1318
                      allowed_values=[constants.VALUE_DEFAULT])
1319

    
1320
  for param in opts.hvparams:
1321
    if isinstance(opts.hvparams[param], basestring):
1322
      if opts.hvparams[param].lower() == "default":
1323
        opts.hvparams[param] = constants.VALUE_DEFAULT
1324

    
1325
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1326
                      allowed_values=[constants.VALUE_DEFAULT])
1327

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

    
1337
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1338

    
1339
  if (opts.disk_template and
1340
      opts.disk_template in constants.DTS_INT_MIRROR and
1341
      not opts.node):
1342
    ToStderr("Changing the disk template to a mirrored one requires"
1343
             " specifying a secondary node")
1344
    return 1
1345

    
1346
  if opts.offline_inst:
1347
    offline = True
1348
  elif opts.online_inst:
1349
    offline = False
1350
  else:
1351
    offline = None
1352

    
1353
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1354
                                   nics=nics,
1355
                                   disks=disks,
1356
                                   hotplug=opts.hotplug,
1357
                                   disk_template=opts.disk_template,
1358
                                   remote_node=opts.node,
1359
                                   pnode=opts.new_primary_node,
1360
                                   hvparams=opts.hvparams,
1361
                                   beparams=opts.beparams,
1362
                                   runtime_mem=opts.runtime_mem,
1363
                                   os_name=opts.os,
1364
                                   osparams=opts.osparams,
1365
                                   force_variant=opts.force_variant,
1366
                                   force=opts.force,
1367
                                   wait_for_sync=opts.wait_for_sync,
1368
                                   offline=offline,
1369
                                   conflicts_check=opts.conflicts_check,
1370
                                   ignore_ipolicy=opts.ignore_ipolicy)
1371

    
1372
  # even if here we process the result, we allow submit only
1373
  result = SubmitOrSend(op, opts)
1374

    
1375
  if result:
1376
    ToStdout("Modified instance %s", args[0])
1377
    for param, data in result:
1378
      ToStdout(" - %-5s -> %s", param, data)
1379
    ToStdout("Please don't forget that most parameters take effect"
1380
             " only at the next (re)start of the instance initiated by"
1381
             " ganeti; restarting from within the instance will"
1382
             " not be enough.")
1383
  return 0
1384

    
1385

    
1386
def ChangeGroup(opts, args):
1387
  """Moves an instance to another group.
1388

1389
  """
1390
  (instance_name, ) = args
1391

    
1392
  cl = GetClient()
1393

    
1394
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1395
                                     iallocator=opts.iallocator,
1396
                                     target_groups=opts.to,
1397
                                     early_release=opts.early_release)
1398
  result = SubmitOrSend(op, opts, cl=cl)
1399

    
1400
  # Keep track of submitted jobs
1401
  jex = JobExecutor(cl=cl, opts=opts)
1402

    
1403
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1404
    jex.AddJobId(None, status, job_id)
1405

    
1406
  results = jex.GetResults()
1407
  bad_cnt = len([row for row in results if not row[0]])
1408
  if bad_cnt == 0:
1409
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1410
    rcode = constants.EXIT_SUCCESS
1411
  else:
1412
    ToStdout("There were %s errors while changing group of instance '%s'.",
1413
             bad_cnt, instance_name)
1414
    rcode = constants.EXIT_FAILURE
1415

    
1416
  return rcode
1417

    
1418

    
1419
# multi-instance selection options
1420
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1421
                           help="Do not ask for confirmation when more than"
1422
                           " one instance is affected",
1423
                           action="store_true", default=False)
1424

    
1425
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1426
                            help="Filter by nodes (primary only)",
1427
                            const=_EXPAND_NODES_PRI, action="store_const")
1428

    
1429
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1430
                            help="Filter by nodes (secondary only)",
1431
                            const=_EXPAND_NODES_SEC, action="store_const")
1432

    
1433
m_node_opt = cli_option("--node", dest="multi_mode",
1434
                        help="Filter by nodes (primary and secondary)",
1435
                        const=_EXPAND_NODES_BOTH, action="store_const")
1436

    
1437
m_clust_opt = cli_option("--all", dest="multi_mode",
1438
                         help="Select all instances in the cluster",
1439
                         const=_EXPAND_CLUSTER, action="store_const")
1440

    
1441
m_inst_opt = cli_option("--instance", dest="multi_mode",
1442
                        help="Filter by instance name [default]",
1443
                        const=_EXPAND_INSTANCES, action="store_const")
1444

    
1445
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1446
                             help="Filter by node tag",
1447
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1448
                             action="store_const")
1449

    
1450
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1451
                                 help="Filter by primary node tag",
1452
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1453
                                 action="store_const")
1454

    
1455
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1456
                                 help="Filter by secondary node tag",
1457
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1458
                                 action="store_const")
1459

    
1460
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1461
                             help="Filter by instance tag",
1462
                             const=_EXPAND_INSTANCES_BY_TAGS,
1463
                             action="store_const")
1464

    
1465
# this is defined separately due to readability only
1466
add_opts = [
1467
  NOSTART_OPT,
1468
  OS_OPT,
1469
  FORCE_VARIANT_OPT,
1470
  NO_INSTALL_OPT,
1471
  IGNORE_IPOLICY_OPT,
1472
  ]
1473

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

    
1627
#: dictionary with aliases for commands
1628
aliases = {
1629
  "start": "startup",
1630
  "stop": "shutdown",
1631
  "show": "info",
1632
  }
1633

    
1634

    
1635
def Main():
1636
  return GenericMain(commands, aliases=aliases,
1637
                     override={"tag_type": constants.TAG_INSTANCE},
1638
                     env_override=_ENV_OVERRIDE)