Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 6a1e009c

History | View | Annotate | Download (56.5 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
from cStringIO import StringIO
33

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

    
45

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

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

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

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

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

    
72

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

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

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

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

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

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

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

    
148
  return inames
149

    
150

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

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

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

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

    
172

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

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

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

    
206

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

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

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

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

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

    
232

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

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

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

    
246

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

250
  This is just a wrapper over GenericInstanceCreate.
251

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

    
255

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

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

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

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

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

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

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

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

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

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

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

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

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

    
316
  return rcode
317

    
318

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
406

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

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

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

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

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

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

    
437

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

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

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

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

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

    
463
  return 0
464

    
465

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

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

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

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

    
490

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

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

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

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

    
510

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

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

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

    
523
  disks = []
524

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

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

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

    
541
      disks.append((didx, ddict))
542

    
543
    # TODO: Verify modifyable parameters (already done in
544
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
545

    
546
  if opts.node:
547
    if opts.iallocator:
548
      msg = "At most one of either --nodes or --iallocator can be passed"
549
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
550
    pnode, snode = SplitNodeOption(opts.node)
551
    nodes = [pnode]
552
    if snode is not None:
553
      nodes.append(snode)
554
  else:
555
    nodes = []
556

    
557
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
558
                                       disks=disks, nodes=nodes,
559
                                       iallocator=opts.iallocator)
560
  SubmitOrSend(op, opts)
561

    
562
  return 0
563

    
564

    
565
def GrowDisk(opts, args):
566
  """Grow an instance's disks.
567

568
  @param opts: the command line options selected by the user
569
  @type args: list
570
  @param args: should contain three elements, the target instance name,
571
      the target disk id, and the target growth
572
  @rtype: int
573
  @return: the desired exit code
574

575
  """
576
  instance = args[0]
577
  disk = args[1]
578
  try:
579
    disk = int(disk)
580
  except (TypeError, ValueError), err:
581
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
582
                               errors.ECODE_INVAL)
583
  try:
584
    amount = utils.ParseUnit(args[2])
585
  except errors.UnitParseError:
586
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
587
                               errors.ECODE_INVAL)
588
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
589
                                  disk=disk, amount=amount,
590
                                  wait_for_sync=opts.wait_for_sync,
591
                                  absolute=opts.absolute)
592
  SubmitOrSend(op, opts)
593
  return 0
594

    
595

    
596
def _StartupInstance(name, opts):
597
  """Startup instances.
598

599
  This returns the opcode to start an instance, and its decorator will
600
  wrap this into a loop starting all desired instances.
601

602
  @param name: the name of the instance to act on
603
  @param opts: the command line options selected by the user
604
  @return: the opcode needed for the operation
605

606
  """
607
  op = opcodes.OpInstanceStartup(instance_name=name,
608
                                 force=opts.force,
609
                                 ignore_offline_nodes=opts.ignore_offline,
610
                                 no_remember=opts.no_remember,
611
                                 startup_paused=opts.startup_paused)
612
  # do not add these parameters to the opcode unless they're defined
613
  if opts.hvparams:
614
    op.hvparams = opts.hvparams
615
  if opts.beparams:
616
    op.beparams = opts.beparams
617
  return op
618

    
619

    
620
def _RebootInstance(name, opts):
621
  """Reboot instance(s).
622

623
  This returns the opcode to reboot an instance, and its decorator
624
  will wrap this into a loop rebooting all desired instances.
625

626
  @param name: the name of the instance to act on
627
  @param opts: the command line options selected by the user
628
  @return: the opcode needed for the operation
629

630
  """
631
  return opcodes.OpInstanceReboot(instance_name=name,
632
                                  reboot_type=opts.reboot_type,
633
                                  ignore_secondaries=opts.ignore_secondaries,
634
                                  shutdown_timeout=opts.shutdown_timeout)
635

    
636

    
637
def _ShutdownInstance(name, opts):
638
  """Shutdown an instance.
639

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

643
  @param name: the name of the instance to act on
644
  @param opts: the command line options selected by the user
645
  @return: the opcode needed for the operation
646

647
  """
648
  return opcodes.OpInstanceShutdown(instance_name=name,
649
                                    force=opts.force,
650
                                    timeout=opts.timeout,
651
                                    ignore_offline_nodes=opts.ignore_offline,
652
                                    no_remember=opts.no_remember)
653

    
654

    
655
def ReplaceDisks(opts, args):
656
  """Replace the disks of an instance
657

658
  @param opts: the command line options selected by the user
659
  @type args: list
660
  @param args: should contain only one element, the instance name
661
  @rtype: int
662
  @return: the desired exit code
663

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

    
693
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
694
                                      remote_node=new_2ndary, mode=mode,
695
                                      iallocator=iallocator,
696
                                      early_release=opts.early_release,
697
                                      ignore_ipolicy=opts.ignore_ipolicy)
698
  SubmitOrSend(op, opts)
699
  return 0
700

    
701

    
702
def FailoverInstance(opts, args):
703
  """Failover an instance.
704

705
  The failover is done by shutting it down on its present node and
706
  starting it on the secondary.
707

708
  @param opts: the command line options selected by the user
709
  @type args: list
710
  @param args: should contain only one element, the instance name
711
  @rtype: int
712
  @return: the desired exit code
713

714
  """
715
  cl = GetClient()
716
  instance_name = args[0]
717
  force = opts.force
718
  iallocator = opts.iallocator
719
  target_node = opts.dst_node
720

    
721
  if iallocator and target_node:
722
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
723
                               " node (-n) but not both", errors.ECODE_INVAL)
724

    
725
  if not force:
726
    _EnsureInstancesExist(cl, [instance_name])
727

    
728
    usertext = ("Failover will happen to image %s."
729
                " This requires a shutdown of the instance. Continue?" %
730
                (instance_name,))
731
    if not AskUser(usertext):
732
      return 1
733

    
734
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
735
                                  ignore_consistency=opts.ignore_consistency,
736
                                  shutdown_timeout=opts.shutdown_timeout,
737
                                  iallocator=iallocator,
738
                                  target_node=target_node,
739
                                  ignore_ipolicy=opts.ignore_ipolicy)
740
  SubmitOrSend(op, opts, cl=cl)
741
  return 0
742

    
743

    
744
def MigrateInstance(opts, args):
745
  """Migrate an instance.
746

747
  The migrate is done without shutdown.
748

749
  @param opts: the command line options selected by the user
750
  @type args: list
751
  @param args: should contain only one element, the instance name
752
  @rtype: int
753
  @return: the desired exit code
754

755
  """
756
  cl = GetClient()
757
  instance_name = args[0]
758
  force = opts.force
759
  iallocator = opts.iallocator
760
  target_node = opts.dst_node
761

    
762
  if iallocator and target_node:
763
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
764
                               " node (-n) but not both", errors.ECODE_INVAL)
765

    
766
  if not force:
767
    _EnsureInstancesExist(cl, [instance_name])
768

    
769
    if opts.cleanup:
770
      usertext = ("Instance %s will be recovered from a failed migration."
771
                  " Note that the migration procedure (including cleanup)" %
772
                  (instance_name,))
773
    else:
774
      usertext = ("Instance %s will be migrated. Note that migration" %
775
                  (instance_name,))
776
    usertext += (" might impact the instance if anything goes wrong"
777
                 " (e.g. due to bugs in the hypervisor). Continue?")
778
    if not AskUser(usertext):
779
      return 1
780

    
781
  # this should be removed once --non-live is deprecated
782
  if not opts.live and opts.migration_mode is not None:
783
    raise errors.OpPrereqError("Only one of the --non-live and "
784
                               "--migration-mode options can be passed",
785
                               errors.ECODE_INVAL)
786
  if not opts.live: # --non-live passed
787
    mode = constants.HT_MIGRATION_NONLIVE
788
  else:
789
    mode = opts.migration_mode
790

    
791
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
792
                                 cleanup=opts.cleanup, iallocator=iallocator,
793
                                 target_node=target_node,
794
                                 allow_failover=opts.allow_failover,
795
                                 allow_runtime_changes=opts.allow_runtime_chgs,
796
                                 ignore_ipolicy=opts.ignore_ipolicy)
797
  SubmitOrSend(op, cl=cl, opts=opts)
798
  return 0
799

    
800

    
801
def MoveInstance(opts, args):
802
  """Move an instance.
803

804
  @param opts: the command line options selected by the user
805
  @type args: list
806
  @param args: should contain only one element, the instance name
807
  @rtype: int
808
  @return: the desired exit code
809

810
  """
811
  cl = GetClient()
812
  instance_name = args[0]
813
  force = opts.force
814

    
815
  if not force:
816
    usertext = ("Instance %s will be moved."
817
                " This requires a shutdown of the instance. Continue?" %
818
                (instance_name,))
819
    if not AskUser(usertext):
820
      return 1
821

    
822
  op = opcodes.OpInstanceMove(instance_name=instance_name,
823
                              target_node=opts.node,
824
                              shutdown_timeout=opts.shutdown_timeout,
825
                              ignore_consistency=opts.ignore_consistency,
826
                              ignore_ipolicy=opts.ignore_ipolicy)
827
  SubmitOrSend(op, opts, cl=cl)
828
  return 0
829

    
830

    
831
def ConnectToInstanceConsole(opts, args):
832
  """Connect to the console of an instance.
833

834
  @param opts: the command line options selected by the user
835
  @type args: list
836
  @param args: should contain only one element, the instance name
837
  @rtype: int
838
  @return: the desired exit code
839

840
  """
841
  instance_name = args[0]
842

    
843
  cl = GetClient()
844
  try:
845
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
846
    ((console_data, oper_state), ) = \
847
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
848
  finally:
849
    # Ensure client connection is closed while external commands are run
850
    cl.Close()
851

    
852
  del cl
853

    
854
  if not console_data:
855
    if oper_state:
856
      # Instance is running
857
      raise errors.OpExecError("Console information for instance %s is"
858
                               " unavailable" % instance_name)
859
    else:
860
      raise errors.OpExecError("Instance %s is not running, can't get console" %
861
                               instance_name)
862

    
863
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
864
                    opts.show_command, cluster_name)
865

    
866

    
867
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
868
               _runcmd_fn=utils.RunCmd):
869
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
870

871
  @type console: L{objects.InstanceConsole}
872
  @param console: Console object
873
  @type show_command: bool
874
  @param show_command: Whether to just display commands
875
  @type cluster_name: string
876
  @param cluster_name: Cluster name as retrieved from master daemon
877

878
  """
879
  assert console.Validate()
880

    
881
  if console.kind == constants.CONS_MESSAGE:
882
    feedback_fn(console.message)
883
  elif console.kind == constants.CONS_VNC:
884
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
885
                " URL <vnc://%s:%s/>",
886
                console.instance, console.host, console.port,
887
                console.display, console.host, console.port)
888
  elif console.kind == constants.CONS_SPICE:
889
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
890
                console.host, console.port)
891
  elif console.kind == constants.CONS_SSH:
892
    # Convert to string if not already one
893
    if isinstance(console.command, basestring):
894
      cmd = console.command
895
    else:
896
      cmd = utils.ShellQuoteArgs(console.command)
897

    
898
    srun = ssh.SshRunner(cluster_name=cluster_name)
899
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
900
                            batch=True, quiet=False, tty=True)
901

    
902
    if show_command:
903
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
904
    else:
905
      result = _runcmd_fn(ssh_cmd, interactive=True)
906
      if result.failed:
907
        logging.error("Console command \"%s\" failed with reason '%s' and"
908
                      " output %r", result.cmd, result.fail_reason,
909
                      result.output)
910
        raise errors.OpExecError("Connection to console of instance %s failed,"
911
                                 " please check cluster configuration" %
912
                                 console.instance)
913
  else:
914
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
915

    
916
  return constants.EXIT_SUCCESS
917

    
918

    
919
def _FormatLogicalID(dev_type, logical_id, roman):
920
  """Formats the logical_id of a disk.
921

922
  """
923
  if dev_type == constants.LD_DRBD8:
924
    node_a, node_b, port, minor_a, minor_b, key = logical_id
925
    data = [
926
      ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
927
                                                            convert=roman))),
928
      ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
929
                                                            convert=roman))),
930
      ("port", compat.TryToRoman(port, convert=roman)),
931
      ("auth key", key),
932
      ]
933
  elif dev_type == constants.LD_LV:
934
    vg_name, lv_name = logical_id
935
    data = ["%s/%s" % (vg_name, lv_name)]
936
  else:
937
    data = [str(logical_id)]
938

    
939
  return data
940

    
941

    
942
def _FormatBlockDevInfo(idx, top_level, dev, roman):
943
  """Show block device information.
944

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

948
  @type idx: int
949
  @param idx: the index of the current disk
950
  @type top_level: boolean
951
  @param top_level: if this a top-level disk?
952
  @type dev: dict
953
  @param dev: dictionary with disk information
954
  @type roman: boolean
955
  @param roman: whether to try to use roman integers
956
  @return: a list of either strings, tuples or lists
957
      (which should be formatted at a higher indent level)
958

959
  """
960
  def helper(dtype, status):
961
    """Format one line for physical device status.
962

963
    @type dtype: str
964
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
965
    @type status: tuple
966
    @param status: a tuple as returned from L{backend.FindBlockDevice}
967
    @return: the string representing the status
968

969
    """
970
    if not status:
971
      return "not active"
972
    txt = ""
973
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
974
    if major is None:
975
      major_string = "N/A"
976
    else:
977
      major_string = str(compat.TryToRoman(major, convert=roman))
978

    
979
    if minor is None:
980
      minor_string = "N/A"
981
    else:
982
      minor_string = str(compat.TryToRoman(minor, convert=roman))
983

    
984
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
985
    if dtype in (constants.LD_DRBD8, ):
986
      if syncp is not None:
987
        sync_text = "*RECOVERING* %5.2f%%," % syncp
988
        if estt:
989
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
990
        else:
991
          sync_text += " ETA unknown"
992
      else:
993
        sync_text = "in sync"
994
      if degr:
995
        degr_text = "*DEGRADED*"
996
      else:
997
        degr_text = "ok"
998
      if ldisk_status == constants.LDS_FAULTY:
999
        ldisk_text = " *MISSING DISK*"
1000
      elif ldisk_status == constants.LDS_UNKNOWN:
1001
        ldisk_text = " *UNCERTAIN STATE*"
1002
      else:
1003
        ldisk_text = ""
1004
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1005
    elif dtype == constants.LD_LV:
1006
      if ldisk_status == constants.LDS_FAULTY:
1007
        ldisk_text = " *FAILED* (failed drive?)"
1008
      else:
1009
        ldisk_text = ""
1010
      txt += ldisk_text
1011
    return txt
1012

    
1013
  # the header
1014
  if top_level:
1015
    if dev["iv_name"] is not None:
1016
      txt = dev["iv_name"]
1017
    else:
1018
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1019
  else:
1020
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1021
  if isinstance(dev["size"], int):
1022
    nice_size = utils.FormatUnit(dev["size"], "h")
1023
  else:
1024
    nice_size = dev["size"]
1025
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1026
  data = []
1027
  if top_level:
1028
    data.append(("access mode", dev["mode"]))
1029
  if dev["logical_id"] is not None:
1030
    try:
1031
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1032
    except ValueError:
1033
      l_id = [str(dev["logical_id"])]
1034
    if len(l_id) == 1:
1035
      data.append(("logical_id", l_id[0]))
1036
    else:
1037
      data.extend(l_id)
1038
  elif dev["physical_id"] is not None:
1039
    data.append("physical_id:")
1040
    data.append([dev["physical_id"]])
1041

    
1042
  if dev["pstatus"]:
1043
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1044

    
1045
  if dev["sstatus"]:
1046
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1047

    
1048
  if dev["children"]:
1049
    data.append("child devices:")
1050
    for c_idx, child in enumerate(dev["children"]):
1051
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1052
  d1.append(data)
1053
  return d1
1054

    
1055

    
1056
def _FormatList(buf, data, indent_level):
1057
  """Formats a list of data at a given indent level.
1058

1059
  If the element of the list is:
1060
    - a string, it is simply formatted as is
1061
    - a tuple, it will be split into key, value and the all the
1062
      values in a list will be aligned all at the same start column
1063
    - a list, will be recursively formatted
1064

1065
  @type buf: StringIO
1066
  @param buf: the buffer into which we write the output
1067
  @param data: the list to format
1068
  @type indent_level: int
1069
  @param indent_level: the indent level to format at
1070

1071
  """
1072
  max_tlen = max([len(elem[0]) for elem in data
1073
                 if isinstance(elem, tuple)] or [0])
1074
  for elem in data:
1075
    if isinstance(elem, basestring):
1076
      buf.write("%*s%s\n" % (2 * indent_level, "", elem))
1077
    elif isinstance(elem, tuple):
1078
      key, value = elem
1079
      spacer = "%*s" % (max_tlen - len(key), "")
1080
      buf.write("%*s%s:%s %s\n" % (2 * indent_level, "", key, spacer, value))
1081
    elif isinstance(elem, list):
1082
      _FormatList(buf, elem, indent_level + 1)
1083

    
1084

    
1085
def ShowInstanceConfig(opts, args):
1086
  """Compute instance run-time status.
1087

1088
  @param opts: the command line options selected by the user
1089
  @type args: list
1090
  @param args: either an empty list, and then we query all
1091
      instances, or should contain a list of instance names
1092
  @rtype: int
1093
  @return: the desired exit code
1094

1095
  """
1096
  if not args and not opts.show_all:
1097
    ToStderr("No instance selected."
1098
             " Please pass in --all if you want to query all instances.\n"
1099
             "Note that this can take a long time on a big cluster.")
1100
    return 1
1101
  elif args and opts.show_all:
1102
    ToStderr("Cannot use --all if you specify instance names.")
1103
    return 1
1104

    
1105
  retcode = 0
1106
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1107
                                   use_locking=not opts.static)
1108
  result = SubmitOpCode(op, opts=opts)
1109
  if not result:
1110
    ToStdout("No instances.")
1111
    return 1
1112

    
1113
  buf = StringIO()
1114
  retcode = 0
1115
  for instance_name in result:
1116
    instance = result[instance_name]
1117
    buf.write("Instance name: %s\n" % instance["name"])
1118
    buf.write("UUID: %s\n" % instance["uuid"])
1119
    buf.write("Serial number: %s\n" %
1120
              compat.TryToRoman(instance["serial_no"],
1121
                                convert=opts.roman_integers))
1122
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
1123
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1124
    buf.write("State: configured to be %s" % instance["config_state"])
1125
    if instance["run_state"]:
1126
      buf.write(", actual state is %s" % instance["run_state"])
1127
    buf.write("\n")
1128
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
1129
    ##          instance["auto_balance"])
1130
    buf.write("  Nodes:\n")
1131
    buf.write("    - primary: %s\n" % instance["pnode"])
1132
    buf.write("      group: %s (UUID %s)\n" %
1133
              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1134
    buf.write("    - secondaries: %s\n" %
1135
              utils.CommaJoin("%s (group %s, group UUID %s)" %
1136
                                (name, group_name, group_uuid)
1137
                              for (name, group_name, group_uuid) in
1138
                                zip(instance["snodes"],
1139
                                    instance["snodes_group_names"],
1140
                                    instance["snodes_group_uuids"])))
1141
    buf.write("  Operating system: %s\n" % instance["os"])
1142
    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
1143
                        level=2)
1144
    if "network_port" in instance:
1145
      buf.write("  Allocated network port: %s\n" %
1146
                compat.TryToRoman(instance["network_port"],
1147
                                  convert=opts.roman_integers))
1148
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1149

    
1150
    # custom VNC console information
1151
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1152
                                                 None)
1153
    if vnc_bind_address:
1154
      port = instance["network_port"]
1155
      display = int(port) - constants.VNC_BASE_PORT
1156
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1157
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1158
                                                   port,
1159
                                                   display)
1160
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1161
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1162
                             (vnc_bind_address, port,
1163
                              instance["pnode"], display))
1164
      else:
1165
        # vnc bind address is a file
1166
        vnc_console_port = "%s:%s" % (instance["pnode"],
1167
                                      vnc_bind_address)
1168
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1169

    
1170
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1171
                        level=2)
1172
    buf.write("  Hardware:\n")
1173
    # deprecated "memory" value, kept for one version for compatibility
1174
    # TODO(ganeti 2.7) remove.
1175
    be_actual = copy.deepcopy(instance["be_actual"])
1176
    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1177
    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
1178
    # TODO(ganeti 2.7) rework the NICs as well
1179
    buf.write("    - NICs:\n")
1180
    for idx, (ip, mac, mode, link, network, _) in enumerate(instance["nics"]):
1181
      buf.write("      - nic/%d: MAC: %s, IP: %s,"
1182
                " mode: %s, link: %s, network: %s\n" %
1183
                (idx, mac, ip, mode, link, network))
1184
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1185
    buf.write("  Disks:\n")
1186

    
1187
    for idx, device in enumerate(instance["disks"]):
1188
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1189
                  opts.roman_integers), 2)
1190

    
1191
  ToStdout(buf.getvalue().rstrip("\n"))
1192
  return retcode
1193

    
1194

    
1195
def _ConvertNicDiskModifications(mods):
1196
  """Converts NIC/disk modifications from CLI to opcode.
1197

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

1203
  @type mods: list of tuples
1204
  @param mods: Modifications as given by command line parser
1205
  @rtype: list of tuples
1206
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1207

1208
  """
1209
  result = []
1210

    
1211
  for (idx, params) in mods:
1212
    if idx == constants.DDM_ADD:
1213
      # Add item as last item (legacy interface)
1214
      action = constants.DDM_ADD
1215
      idxno = -1
1216
    elif idx == constants.DDM_REMOVE:
1217
      # Remove last item (legacy interface)
1218
      action = constants.DDM_REMOVE
1219
      idxno = -1
1220
    else:
1221
      # Modifications and adding/removing at arbitrary indices
1222
      try:
1223
        idxno = int(idx)
1224
      except (TypeError, ValueError):
1225
        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
1226
                                   errors.ECODE_INVAL)
1227

    
1228
      add = params.pop(constants.DDM_ADD, _MISSING)
1229
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1230
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1231

    
1232
      if modify is _MISSING:
1233
        if not (add is _MISSING or remove is _MISSING):
1234
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1235
                                     errors.ECODE_INVAL)
1236
        elif add is not _MISSING:
1237
          action = constants.DDM_ADD
1238
        elif remove is not _MISSING:
1239
          action = constants.DDM_REMOVE
1240
        else:
1241
          action = constants.DDM_MODIFY
1242

    
1243
      elif add is _MISSING and remove is _MISSING:
1244
        action = constants.DDM_MODIFY
1245
      else:
1246
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1247
                                   " same time", errors.ECODE_INVAL)
1248

    
1249
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1250

    
1251
    if action == constants.DDM_REMOVE and params:
1252
      raise errors.OpPrereqError("Not accepting parameters on removal",
1253
                                 errors.ECODE_INVAL)
1254

    
1255
    result.append((action, idxno, params))
1256

    
1257
  return result
1258

    
1259

    
1260
def _ParseDiskSizes(mods):
1261
  """Parses disk sizes in parameters.
1262

1263
  """
1264
  for (action, _, params) in mods:
1265
    if params and constants.IDISK_SIZE in params:
1266
      params[constants.IDISK_SIZE] = \
1267
        utils.ParseUnit(params[constants.IDISK_SIZE])
1268
    elif action == constants.DDM_ADD:
1269
      raise errors.OpPrereqError("Missing required parameter 'size'",
1270
                                 errors.ECODE_INVAL)
1271

    
1272
  return mods
1273

    
1274

    
1275
def SetInstanceParams(opts, args):
1276
  """Modifies an instance.
1277

1278
  All parameters take effect only at the next restart of the instance.
1279

1280
  @param opts: the command line options selected by the user
1281
  @type args: list
1282
  @param args: should contain only one element, the instance name
1283
  @rtype: int
1284
  @return: the desired exit code
1285

1286
  """
1287
  if not (opts.nics or opts.disks or opts.disk_template or
1288
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1289
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1290
    ToStderr("Please give at least one of the parameters.")
1291
    return 1
1292

    
1293
  for param in opts.beparams:
1294
    if isinstance(opts.beparams[param], basestring):
1295
      if opts.beparams[param].lower() == "default":
1296
        opts.beparams[param] = constants.VALUE_DEFAULT
1297

    
1298
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1299
                      allowed_values=[constants.VALUE_DEFAULT])
1300

    
1301
  for param in opts.hvparams:
1302
    if isinstance(opts.hvparams[param], basestring):
1303
      if opts.hvparams[param].lower() == "default":
1304
        opts.hvparams[param] = constants.VALUE_DEFAULT
1305

    
1306
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1307
                      allowed_values=[constants.VALUE_DEFAULT])
1308

    
1309
  nics = _ConvertNicDiskModifications(opts.nics)
1310
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1311

    
1312
  if (opts.disk_template and
1313
      opts.disk_template in constants.DTS_INT_MIRROR and
1314
      not opts.node):
1315
    ToStderr("Changing the disk template to a mirrored one requires"
1316
             " specifying a secondary node")
1317
    return 1
1318

    
1319
  if opts.offline_inst:
1320
    offline = True
1321
  elif opts.online_inst:
1322
    offline = False
1323
  else:
1324
    offline = None
1325

    
1326
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1327
                                   nics=nics,
1328
                                   disks=disks,
1329
                                   hotplug=opts.hotplug,
1330
                                   disk_template=opts.disk_template,
1331
                                   remote_node=opts.node,
1332
                                   hvparams=opts.hvparams,
1333
                                   beparams=opts.beparams,
1334
                                   runtime_mem=opts.runtime_mem,
1335
                                   os_name=opts.os,
1336
                                   osparams=opts.osparams,
1337
                                   force_variant=opts.force_variant,
1338
                                   force=opts.force,
1339
                                   wait_for_sync=opts.wait_for_sync,
1340
                                   offline=offline,
1341
                                   conflicts_check=opts.conflicts_check,
1342
                                   ignore_ipolicy=opts.ignore_ipolicy)
1343

    
1344
  # even if here we process the result, we allow submit only
1345
  result = SubmitOrSend(op, opts)
1346

    
1347
  if result:
1348
    ToStdout("Modified instance %s", args[0])
1349
    for param, data in result:
1350
      ToStdout(" - %-5s -> %s", param, data)
1351
    if not opts.hotplug:
1352
      ToStdout("Please don't forget that most parameters take effect"
1353
               " only at the next (re)start of the instance initiated by"
1354
               " ganeti; restarting from within the instance will"
1355
               " not be enough.")
1356
  return 0
1357

    
1358

    
1359
def ChangeGroup(opts, args):
1360
  """Moves an instance to another group.
1361

1362
  """
1363
  (instance_name, ) = args
1364

    
1365
  cl = GetClient()
1366

    
1367
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1368
                                     iallocator=opts.iallocator,
1369
                                     target_groups=opts.to,
1370
                                     early_release=opts.early_release)
1371
  result = SubmitOrSend(op, opts, cl=cl)
1372

    
1373
  # Keep track of submitted jobs
1374
  jex = JobExecutor(cl=cl, opts=opts)
1375

    
1376
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1377
    jex.AddJobId(None, status, job_id)
1378

    
1379
  results = jex.GetResults()
1380
  bad_cnt = len([row for row in results if not row[0]])
1381
  if bad_cnt == 0:
1382
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1383
    rcode = constants.EXIT_SUCCESS
1384
  else:
1385
    ToStdout("There were %s errors while changing group of instance '%s'.",
1386
             bad_cnt, instance_name)
1387
    rcode = constants.EXIT_FAILURE
1388

    
1389
  return rcode
1390

    
1391

    
1392
# multi-instance selection options
1393
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1394
                           help="Do not ask for confirmation when more than"
1395
                           " one instance is affected",
1396
                           action="store_true", default=False)
1397

    
1398
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1399
                            help="Filter by nodes (primary only)",
1400
                            const=_EXPAND_NODES_PRI, action="store_const")
1401

    
1402
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1403
                            help="Filter by nodes (secondary only)",
1404
                            const=_EXPAND_NODES_SEC, action="store_const")
1405

    
1406
m_node_opt = cli_option("--node", dest="multi_mode",
1407
                        help="Filter by nodes (primary and secondary)",
1408
                        const=_EXPAND_NODES_BOTH, action="store_const")
1409

    
1410
m_clust_opt = cli_option("--all", dest="multi_mode",
1411
                         help="Select all instances in the cluster",
1412
                         const=_EXPAND_CLUSTER, action="store_const")
1413

    
1414
m_inst_opt = cli_option("--instance", dest="multi_mode",
1415
                        help="Filter by instance name [default]",
1416
                        const=_EXPAND_INSTANCES, action="store_const")
1417

    
1418
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1419
                             help="Filter by node tag",
1420
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1421
                             action="store_const")
1422

    
1423
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1424
                                 help="Filter by primary node tag",
1425
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1426
                                 action="store_const")
1427

    
1428
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1429
                                 help="Filter by secondary node tag",
1430
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1431
                                 action="store_const")
1432

    
1433
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1434
                             help="Filter by instance tag",
1435
                             const=_EXPAND_INSTANCES_BY_TAGS,
1436
                             action="store_const")
1437

    
1438
# this is defined separately due to readability only
1439
add_opts = [
1440
  NOSTART_OPT,
1441
  OS_OPT,
1442
  FORCE_VARIANT_OPT,
1443
  NO_INSTALL_OPT,
1444
  IGNORE_IPOLICY_OPT,
1445
  ]
1446

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

    
1593
#: dictionary with aliases for commands
1594
aliases = {
1595
  "start": "startup",
1596
  "stop": "shutdown",
1597
  "show": "info",
1598
  }
1599

    
1600

    
1601
def Main():
1602
  return GenericMain(commands, aliases=aliases,
1603
                     override={"tag_type": constants.TAG_INSTANCE},
1604
                     env_override=_ENV_OVERRIDE)