gnt_instance.py 51.2 KB
Newer Older
1
#
Iustin Pop's avatar
Iustin Pop committed
2
3
#

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

Iustin Pop's avatar
Iustin Pop committed
21
"""Instance related commands"""
Iustin Pop's avatar
Iustin Pop committed
22

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

28
import itertools
29
import simplejson
30
import logging
Iustin Pop's avatar
Iustin Pop committed
31
32
33
34
35
from cStringIO import StringIO

from ganeti.cli import *
from ganeti import opcodes
from ganeti import constants
36
from ganeti import compat
Iustin Pop's avatar
Iustin Pop committed
37
from ganeti import utils
38
from ganeti import errors
39
from ganeti import netutils
40
41
from ganeti import ssh
from ganeti import objects
42
43
44
45
46
47


_SHUTDOWN_CLUSTER = "cluster"
_SHUTDOWN_NODES_BOTH = "nodes"
_SHUTDOWN_NODES_PRI = "nodes-pri"
_SHUTDOWN_NODES_SEC = "nodes-sec"
48
49
50
_SHUTDOWN_NODES_BOTH_BY_TAGS = "nodes-by-tags"
_SHUTDOWN_NODES_PRI_BY_TAGS = "nodes-pri-by-tags"
_SHUTDOWN_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
51
_SHUTDOWN_INSTANCES = "instances"
52
53
54
55
56
57
_SHUTDOWN_INSTANCES_BY_TAGS = "instances-by-tags"

_SHUTDOWN_NODES_TAGS_MODES = (
    _SHUTDOWN_NODES_BOTH_BY_TAGS,
    _SHUTDOWN_NODES_PRI_BY_TAGS,
    _SHUTDOWN_NODES_SEC_BY_TAGS)
58

59

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

65

66
def _ExpandMultiNames(mode, names, client=None):
67
68
69
70
  """Expand the given names using the passed mode.

  For _SHUTDOWN_CLUSTER, all instances will be returned. For
  _SHUTDOWN_NODES_PRI/SEC, all instances having those nodes as
71
  primary/secondary will be returned. For _SHUTDOWN_NODES_BOTH, all
72
73
74
75
  instances having those nodes as either primary or secondary will be
  returned. For _SHUTDOWN_INSTANCES, the given instances will be
  returned.

76
77
78
79
80
81
82
83
84
85
86
87
  @param mode: one of L{_SHUTDOWN_CLUSTER}, L{_SHUTDOWN_NODES_BOTH},
      L{_SHUTDOWN_NODES_PRI}, L{_SHUTDOWN_NODES_SEC} or
      L{_SHUTDOWN_INSTANCES}
  @param names: a list of names; for cluster, it must be empty,
      and for node and instance it must be a list of valid item
      names (short names are valid as usual, e.g. node1 instead of
      node1.example.com)
  @rtype: list
  @return: the list of names after the expansion
  @raise errors.ProgrammerError: for unknown selection type
  @raise errors.OpPrereqError: for invalid input parameters

88
  """
Iustin Pop's avatar
Iustin Pop committed
89
  # pylint: disable-msg=W0142
90

91
92
  if client is None:
    client = GetClient()
93
94
  if mode == _SHUTDOWN_CLUSTER:
    if names:
95
96
      raise errors.OpPrereqError("Cluster filter mode takes no arguments",
                                 errors.ECODE_INVAL)
97
    idata = client.QueryInstances([], ["name"], False)
98
99
100
101
    inames = [row[0] for row in idata]

  elif mode in (_SHUTDOWN_NODES_BOTH,
                _SHUTDOWN_NODES_PRI,
102
103
104
105
106
107
108
109
110
111
112
                _SHUTDOWN_NODES_SEC) + _SHUTDOWN_NODES_TAGS_MODES:
    if mode in _SHUTDOWN_NODES_TAGS_MODES:
      if not names:
        raise errors.OpPrereqError("No node tags passed", errors.ECODE_INVAL)
      ndata = client.QueryNodes([], ["name", "pinst_list",
                                     "sinst_list", "tags"], False)
      ndata = [row for row in ndata if set(row[3]).intersection(names)]
    else:
      if not names:
        raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
      ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
113
                              False)
114

115
116
117
118
    ipri = [row[1] for row in ndata]
    pri_names = list(itertools.chain(*ipri))
    isec = [row[2] for row in ndata]
    sec_names = list(itertools.chain(*isec))
119
    if mode in (_SHUTDOWN_NODES_BOTH, _SHUTDOWN_NODES_BOTH_BY_TAGS):
120
      inames = pri_names + sec_names
121
    elif mode in (_SHUTDOWN_NODES_PRI, _SHUTDOWN_NODES_PRI_BY_TAGS):
122
      inames = pri_names
123
    elif mode in (_SHUTDOWN_NODES_SEC, _SHUTDOWN_NODES_SEC_BY_TAGS):
124
125
126
127
128
      inames = sec_names
    else:
      raise errors.ProgrammerError("Unhandled shutdown type")
  elif mode == _SHUTDOWN_INSTANCES:
    if not names:
129
130
      raise errors.OpPrereqError("No instance names passed",
                                 errors.ECODE_INVAL)
131
    idata = client.QueryInstances(names, ["name"], False)
132
    inames = [row[0] for row in idata]
133
134
135
136
137
138
  elif mode == _SHUTDOWN_INSTANCES_BY_TAGS:
    if not names:
      raise errors.OpPrereqError("No instance tags passed",
                                 errors.ECODE_INVAL)
    idata = client.QueryInstances([], ["name", "tags"], False)
    inames = [row[0] for row in idata if set(row[1]).intersection(names)]
139
  else:
140
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
141
142

  return inames
Iustin Pop's avatar
Iustin Pop committed
143
144


145
146
147
148
149
150
def _EnsureInstancesExist(client, names):
  """Check for and ensure the given instance names exist.

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

Iustin Pop's avatar
Iustin Pop committed
151
  @type client: L{ganeti.luxi.Client}
152
153
154
155
156
157
  @param client: the client to use for the query
  @type names: list
  @param names: the list of instance names to query
  @raise errors.OpPrereqError: in case any instance is missing

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


167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def GenericManyOps(operation, fn):
  """Generic multi-instance operations.

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

  """
  def realfn(opts, args):
    if opts.multi_mode is None:
      opts.multi_mode = _SHUTDOWN_INSTANCES
    cl = GetClient()
    inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
    if not inames:
      raise errors.OpPrereqError("Selection filter does not match"
183
                                 " any instances", errors.ECODE_INVAL)
184
185
    multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
    if not (opts.force_multi or not multi_on
186
            or ConfirmOperation(inames, "instances", operation)):
187
      return 1
188
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
189
190
191
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
Iustin Pop's avatar
Iustin Pop committed
192
193
194
    results = jex.WaitOrShow(not opts.submit_only)
    rcode = compat.all(row[0] for row in results)
    return int(not rcode)
195
196
197
  return realfn


Iustin Pop's avatar
Iustin Pop committed
198
def ListInstances(opts, args):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
199
  """List instances and their properties.
Iustin Pop's avatar
Iustin Pop committed
200

201
202
203
204
205
206
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
207
  """
208
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
Iustin Pop's avatar
Iustin Pop committed
209

210
211
212
213
214
215
  fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
                               "nic.modes", "nic.links", "nic.bridges",
                               "snodes"],
                              (lambda value: ",".join(str(item)
                                                      for item in value),
                               False))
Iustin Pop's avatar
Iustin Pop committed
216

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
                     opts.separator, not opts.no_headers,
                     format_override=fmtoverride)


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

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

  """
  return GenericListFields(constants.QR_INSTANCE, args, opts.separator,
                           not opts.no_headers)
Iustin Pop's avatar
Iustin Pop committed
234
235
236
237
238


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

Iustin Pop's avatar
Iustin Pop committed
239
  This is just a wrapper over GenericInstanceCreate.
Iustin Pop's avatar
Iustin Pop committed
240
241

  """
Iustin Pop's avatar
Iustin Pop committed
242
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
Iustin Pop's avatar
Iustin Pop committed
243
244


245
def BatchCreate(opts, args):
246
247
248
249
250
251
  """Create instances using a definition file.

  This function reads a json file with instances defined
  in the form::

    {"instance-name":{
252
      "disk_size": [20480],
253
254
255
256
      "template": "drbd",
      "backend": {
        "memory": 512,
        "vcpus": 1 },
257
      "os": "debootstrap",
258
259
260
261
262
263
264
265
266
267
268
269
270
      "primary_node": "firstnode",
      "secondary_node": "secondnode",
      "iallocator": "dumb"}
    }

  Note that I{primary_node} and I{secondary_node} have precedence over
  I{iallocator}.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain one element, the json filename
  @rtype: int
  @return: the desired exit code
271
272

  """
273
  _DEFAULT_SPECS = {"disk_size": [20 * 1024],
274
275
276
277
                    "backend": {},
                    "iallocator": None,
                    "primary_node": None,
                    "secondary_node": None,
278
                    "nics": None,
279
280
                    "start": True,
                    "ip_check": True,
281
                    "name_check": True,
282
                    "hypervisor": None,
Iustin Pop's avatar
Iustin Pop committed
283
                    "hvparams": {},
284
                    "file_storage_dir": None,
285
                    "force_variant": False,
286
287
288
289
                    "file_driver": 'loop'}

  def _PopulateWithDefaults(spec):
    """Returns a new hash combined with default values."""
290
291
292
    mydict = _DEFAULT_SPECS.copy()
    mydict.update(spec)
    return mydict
293
294
295
296
297
298
299

  def _Validate(spec):
    """Validate the instance specs."""
    # Validate fields required under any circumstances
    for required_field in ('os', 'template'):
      if required_field not in spec:
        raise errors.OpPrereqError('Required field "%s" is missing.' %
300
                                   required_field, errors.ECODE_INVAL)
301
302
303
304
305
    # Validate special fields
    if spec['primary_node'] is not None:
      if (spec['template'] in constants.DTS_NET_MIRROR and
          spec['secondary_node'] is None):
        raise errors.OpPrereqError('Template requires secondary node, but'
306
307
                                   ' there was no secondary provided.',
                                   errors.ECODE_INVAL)
308
309
    elif spec['iallocator'] is None:
      raise errors.OpPrereqError('You have to provide at least a primary_node'
310
311
                                 ' or an iallocator.',
                                 errors.ECODE_INVAL)
312

Iustin Pop's avatar
Iustin Pop committed
313
314
    if (spec['hvparams'] and
        not isinstance(spec['hvparams'], dict)):
315
316
      raise errors.OpPrereqError('Hypervisor parameters must be a dict.',
                                 errors.ECODE_INVAL)
317
318
319

  json_filename = args[0]
  try:
320
    instance_data = simplejson.loads(utils.ReadFile(json_filename))
Iustin Pop's avatar
Iustin Pop committed
321
  except Exception, err: # pylint: disable-msg=W0703
Iustin Pop's avatar
Iustin Pop committed
322
323
    ToStderr("Can't parse the instance definition file: %s" % str(err))
    return 1
324

Guido Trotter's avatar
Guido Trotter committed
325
326
327
328
  if not isinstance(instance_data, dict):
    ToStderr("The instance definition file is not in dict format.")
    return 1

329
  jex = JobExecutor(opts=opts)
330

331
332
333
  # Iterate over the instances and do:
  #  * Populate the specs with default value
  #  * Validate the instance specs
Guido Trotter's avatar
Guido Trotter committed
334
  i_names = utils.NiceSort(instance_data.keys()) # pylint: disable-msg=E1103
335
336
  for name in i_names:
    specs = instance_data[name]
337
338
339
    specs = _PopulateWithDefaults(specs)
    _Validate(specs)

Iustin Pop's avatar
Iustin Pop committed
340
341
    hypervisor = specs['hypervisor']
    hvparams = specs['hvparams']
342

343
344
345
346
    disks = []
    for elem in specs['disk_size']:
      try:
        size = utils.ParseUnit(elem)
347
      except (TypeError, ValueError), err:
348
349
        raise errors.OpPrereqError("Invalid disk size '%s' for"
                                   " instance %s: %s" %
350
                                   (elem, name, err), errors.ECODE_INVAL)
351
352
      disks.append({"size": size})

353
354
355
    utils.ForceDictType(specs['backend'], constants.BES_PARAMETER_TYPES)
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

356
357
358
359
360
361
362
363
364
    tmp_nics = []
    for field in ('ip', 'mac', 'mode', 'link', 'bridge'):
      if field in specs:
        if not tmp_nics:
          tmp_nics.append({})
        tmp_nics[0][field] = specs[field]

    if specs['nics'] is not None and tmp_nics:
      raise errors.OpPrereqError("'nics' list incompatible with using"
365
366
                                 " individual nic fields as well",
                                 errors.ECODE_INVAL)
367
368
369
370
371
    elif specs['nics'] is not None:
      tmp_nics = specs['nics']
    elif not tmp_nics:
      tmp_nics = [{}]

372
    op = opcodes.OpInstanceCreate(instance_name=name,
373
                                  disks=disks,
374
375
376
                                  disk_template=specs['template'],
                                  mode=constants.INSTANCE_CREATE,
                                  os_type=specs['os'],
377
                                  force_variant=specs["force_variant"],
378
379
                                  pnode=specs['primary_node'],
                                  snode=specs['secondary_node'],
380
                                  nics=tmp_nics,
381
382
                                  start=specs['start'],
                                  ip_check=specs['ip_check'],
383
                                  name_check=specs['name_check'],
384
385
386
387
388
389
390
391
                                  wait_for_sync=True,
                                  iallocator=specs['iallocator'],
                                  hypervisor=hypervisor,
                                  hvparams=hvparams,
                                  beparams=specs['backend'],
                                  file_storage_dir=specs['file_storage_dir'],
                                  file_driver=specs['file_driver'])

392
393
394
    jex.QueueJob(name, op)
  # we never want to wait, just show the submitted job IDs
  jex.WaitOrShow(False)
395
396
397
398

  return 0


399
400
401
def ReinstallInstance(opts, args):
  """Reinstall an instance.

402
403
404
405
406
407
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the name of the
      instance to be reinstalled
  @rtype: int
  @return: the desired exit code
408
409

  """
410
411
412
413
414
415
  # first, compute the desired name list
  if opts.multi_mode is None:
    opts.multi_mode = _SHUTDOWN_INSTANCES

  inames = _ExpandMultiNames(opts.multi_mode, args)
  if not inames:
416
417
    raise errors.OpPrereqError("Selection filter does not match any instances",
                               errors.ECODE_INVAL)
418

419
  # second, if requested, ask for an OS
420
  if opts.select_os is True:
421
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
422
    result = SubmitOpCode(op, opts=opts)
423
424

    if not result:
425
      ToStdout("Can't get the OS list")
426
427
      return 1

428
    ToStdout("Available OS templates:")
429
430
    number = 0
    choices = []
431
432
433
434
435
    for (name, variants) in result:
      for entry in CalculateOSNames(name, variants):
        ToStdout("%3s: %s", number, entry)
        choices.append(("%s" % number, entry, entry))
        number += 1
436
437

    choices.append(('x', 'exit', 'Exit gnt-instance reinstall'))
438
    selected = AskUser("Enter OS template number (or x to abort):",
439
440
441
                       choices)

    if selected == 'exit':
442
      ToStderr("User aborted reinstall, exiting")
443
444
      return 1

445
    os_name = selected
446
  else:
447
    os_name = opts.os
448

449
450
451
  # third, get confirmation: multi-reinstall requires --force-multi,
  # single-reinstall either --force or --force-multi (--force-multi is
  # a stronger --force)
452
453
454
  multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
  if multi_on:
    warn_msg = "Note: this will remove *all* data for the below instances!\n"
455
    if not (opts.force_multi or
456
            ConfirmOperation(inames, "instances", "reinstall", extra=warn_msg)):
457
      return 1
458
  else:
459
    if not (opts.force or opts.force_multi):
460
      usertext = ("This will reinstall the instance %s and remove"
Iustin Pop's avatar
Iustin Pop committed
461
                  " all data. Continue?") % inames[0]
462
463
464
      if not AskUser(usertext):
        return 1

465
  jex = JobExecutor(verbose=multi_on, opts=opts)
466
  for instance_name in inames:
467
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
468
                                     os_type=os_name,
469
470
                                     force_variant=opts.force_variant,
                                     osparams=opts.osparams)
471
    jex.QueueJob(instance_name, op)
472

473
  jex.WaitOrShow(not opts.submit_only)
474
475
476
  return 0


Iustin Pop's avatar
Iustin Pop committed
477
478
479
def RemoveInstance(opts, args):
  """Remove an instance.

480
481
482
483
484
485
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the name of
      the instance to be removed
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
486
487
488
489

  """
  instance_name = args[0]
  force = opts.force
490
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
491
492

  if not force:
493
494
    _EnsureInstancesExist(cl, [instance_name])

Iustin Pop's avatar
Iustin Pop committed
495
496
497
    usertext = ("This will remove the volumes of the instance %s"
                " (including mirrors), thus removing all the data"
                " of the instance. Continue?") % instance_name
498
    if not AskUser(usertext):
Iustin Pop's avatar
Iustin Pop committed
499
500
      return 1

501
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
502
                                ignore_failures=opts.ignore_failures,
503
                                shutdown_timeout=opts.shutdown_timeout)
504
  SubmitOrSend(op, opts, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
505
506
507
  return 0


508
def RenameInstance(opts, args):
Guido Trotter's avatar
Guido Trotter committed
509
  """Rename an instance.
510

511
512
513
514
515
516
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain two elements, the old and the
      new instance names
  @rtype: int
  @return: the desired exit code
517
518

  """
519
  if not opts.name_check:
520
521
522
523
    if not AskUser("As you disabled the check of the DNS entry, please verify"
                   " that '%s' is a FQDN. Continue?" % args[1]):
      return 1

524
  op = opcodes.OpInstanceRename(instance_name=args[0],
525
                                new_name=args[1],
526
527
                                ip_check=opts.ip_check,
                                name_check=opts.name_check)
528
529
  result = SubmitOrSend(op, opts)

530
531
  if result:
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
532

533
534
535
  return 0


Iustin Pop's avatar
Iustin Pop committed
536
537
538
539
def ActivateDisks(opts, args):
  """Activate an instance's disks.

  This serves two purposes:
540
541
    - it allows (as long as the instance is not running)
      mounting the disks and modifying them from the node
Iustin Pop's avatar
Iustin Pop committed
542
543
    - it repairs inactive secondary drbds

544
545
546
547
548
549
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
550
551
  """
  instance_name = args[0]
552
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
553
                                       ignore_size=opts.ignore_size)
554
  disks_info = SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
555
  for host, iname, nname in disks_info:
556
    ToStdout("%s:%s:%s", host, iname, nname)
Iustin Pop's avatar
Iustin Pop committed
557
558
559
560
  return 0


def DeactivateDisks(opts, args):
Iustin Pop's avatar
Iustin Pop committed
561
  """Deactivate an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
562
563
564
565

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

566
567
568
569
570
571
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
572
573
  """
  instance_name = args[0]
574
575
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
                                         force=opts.force)
576
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
577
578
579
  return 0


Iustin Pop's avatar
Iustin Pop committed
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
def RecreateDisks(opts, args):
  """Recreate an instance's disks.

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

  """
  instance_name = args[0]
  if opts.disks:
    try:
      opts.disks = [int(v) for v in opts.disks.split(",")]
    except (ValueError, TypeError), err:
      ToStderr("Invalid disks value: %s" % str(err))
      return 1
  else:
    opts.disks = []

600
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
Iustin Pop's avatar
Iustin Pop committed
601
602
603
604
605
                                       disks=opts.disks)
  SubmitOrSend(op, opts)
  return 0


Iustin Pop's avatar
Iustin Pop committed
606
def GrowDisk(opts, args):
607
  """Grow an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
608

609
610
611
612
613
614
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain two elements, the instance name
      whose disks we grow and the disk name, e.g. I{sda}
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
615
616
617
618

  """
  instance = args[0]
  disk = args[1]
619
620
  try:
    disk = int(disk)
621
  except (TypeError, ValueError), err:
622
623
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
                               errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
624
  amount = utils.ParseUnit(args[2])
Iustin Pop's avatar
Iustin Pop committed
625
626
627
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
                                  disk=disk, amount=amount,
                                  wait_for_sync=opts.wait_for_sync)
628
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
629
630
631
  return 0


632
def _StartupInstance(name, opts):
633
  """Startup instances.
Iustin Pop's avatar
Iustin Pop committed
634

635
636
  This returns the opcode to start an instance, and its decorator will
  wrap this into a loop starting all desired instances.
637

638
  @param name: the name of the instance to act on
639
  @param opts: the command line options selected by the user
640
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
641
642

  """
643
  op = opcodes.OpInstanceStartup(instance_name=name,
644
645
                                 force=opts.force,
                                 ignore_offline_nodes=opts.ignore_offline)
646
647
648
649
650
651
  # do not add these parameters to the opcode unless they're defined
  if opts.hvparams:
    op.hvparams = opts.hvparams
  if opts.beparams:
    op.beparams = opts.beparams
  return op
Iustin Pop's avatar
Iustin Pop committed
652

653

654
def _RebootInstance(name, opts):
655
656
  """Reboot instance(s).

657
658
  This returns the opcode to reboot an instance, and its decorator
  will wrap this into a loop rebooting all desired instances.
659

660
  @param name: the name of the instance to act on
661
  @param opts: the command line options selected by the user
662
  @return: the opcode needed for the operation
663
664

  """
665
  return opcodes.OpInstanceReboot(instance_name=name,
666
                                  reboot_type=opts.reboot_type,
667
                                  ignore_secondaries=opts.ignore_secondaries,
668
                                  shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
669

670

671
def _ShutdownInstance(name, opts):
Iustin Pop's avatar
Iustin Pop committed
672
673
  """Shutdown an instance.

674
675
676
677
  This returns the opcode to shutdown an instance, and its decorator
  will wrap this into a loop shutting down all desired instances.

  @param name: the name of the instance to act on
678
  @param opts: the command line options selected by the user
679
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
680
681

  """
682
  return opcodes.OpInstanceShutdown(instance_name=name,
683
684
                                    timeout=opts.timeout,
                                    ignore_offline_nodes=opts.ignore_offline)
Iustin Pop's avatar
Iustin Pop committed
685
686
687
688
689


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

690
691
692
693
694
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
695
696

  """
697
  new_2ndary = opts.dst_node
698
  iallocator = opts.iallocator
699
  if opts.disks is None:
700
    disks = []
701
  else:
702
703
    try:
      disks = [int(i) for i in opts.disks.split(",")]
704
    except (TypeError, ValueError), err:
705
706
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
                                 errors.ECODE_INVAL)
707
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
708
709
         new_2ndary is not None, iallocator is not None].count(True)
  if cnt != 1:
710
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -i"
711
                               " options must be passed", errors.ECODE_INVAL)
712
  elif opts.on_primary:
713
    mode = constants.REPLACE_DISK_PRI
714
  elif opts.on_secondary:
715
    mode = constants.REPLACE_DISK_SEC
716
717
718
719
  elif opts.auto:
    mode = constants.REPLACE_DISK_AUTO
    if disks:
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
720
                                 " mode", errors.ECODE_INVAL)
721
722
723
  elif new_2ndary is not None or iallocator is not None:
    # replace secondary
    mode = constants.REPLACE_DISK_CHG
724

725
726
727
728
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
                                      remote_node=new_2ndary, mode=mode,
                                      iallocator=iallocator,
                                      early_release=opts.early_release)
729
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
730
731
732
733
734
735
736
737
738
  return 0


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

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

739
740
741
742
743
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
744
745

  """
746
  cl = GetClient()
747
748
  instance_name = args[0]
  force = opts.force
Iustin Pop's avatar
Iustin Pop committed
749

750
  if not force:
751
752
    _EnsureInstancesExist(cl, [instance_name])

753
754
755
756
757
    usertext = ("Failover will happen to image %s."
                " This requires a shutdown of the instance. Continue?" %
                (instance_name,))
    if not AskUser(usertext):
      return 1
Iustin Pop's avatar
Iustin Pop committed
758

759
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
760
                                  ignore_consistency=opts.ignore_consistency,
761
                                  shutdown_timeout=opts.shutdown_timeout)
762
  SubmitOrSend(op, opts, cl=cl)
763
  return 0
Iustin Pop's avatar
Iustin Pop committed
764
765


766
767
768
769
770
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
771
772
773
774
775
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code
776
777

  """
778
  cl = GetClient()
779
780
781
782
  instance_name = args[0]
  force = opts.force

  if not force:
783
784
    _EnsureInstancesExist(cl, [instance_name])

785
786
787
788
789
790
791
    if opts.cleanup:
      usertext = ("Instance %s will be recovered from a failed migration."
                  " Note that the migration procedure (including cleanup)" %
                  (instance_name,))
    else:
      usertext = ("Instance %s will be migrated. Note that migration" %
                  (instance_name,))
Iustin Pop's avatar
Iustin Pop committed
792
793
    usertext += (" might impact the instance if anything goes wrong"
                 " (e.g. due to bugs in the hypervisor). Continue?")
794
795
796
    if not AskUser(usertext):
      return 1

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

807
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
808
                                 cleanup=opts.cleanup)
809
  SubmitOpCode(op, cl=cl, opts=opts)
810
811
812
  return 0


Iustin Pop's avatar
Iustin Pop committed
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
def MoveInstance(opts, args):
  """Move an instance.

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

  """
  cl = GetClient()
  instance_name = args[0]
  force = opts.force

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

834
  op = opcodes.OpInstanceMove(instance_name=instance_name,
835
                              target_node=opts.node,
836
                              shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
837
838
839
840
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
841
842
843
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

844
845
846
847
848
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the instance name
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
849
850
851
852

  """
  instance_name = args[0]

853
  op = opcodes.OpInstanceConsole(instance_name=instance_name)
854

855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
  cl = GetClient()
  try:
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
    console_data = SubmitOpCode(op, opts=opts, cl=cl)
  finally:
    # Ensure client connection is closed while external commands are run
    cl.Close()

  del cl

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


def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
               _runcmd_fn=utils.RunCmd):
871
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911

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

  """
  assert console.Validate()

  if console.kind == constants.CONS_MESSAGE:
    feedback_fn(console.message)
  elif console.kind == constants.CONS_VNC:
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
                " URL <vnc://%s:%s/>",
                console.instance, console.host, console.port,
                console.display, console.host, console.port)
  elif console.kind == constants.CONS_SSH:
    # Convert to string if not already one
    if isinstance(console.command, basestring):
      cmd = console.command
    else:
      cmd = utils.ShellQuoteArgs(console.command)

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

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

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
916
917


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

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

  return data


941
def _FormatBlockDevInfo(idx, top_level, dev, static, roman):
Iustin Pop's avatar
Iustin Pop committed
942
943
  """Show block device information.

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

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

Iustin Pop's avatar
Iustin Pop committed
961
  """
962
  def helper(dtype, status):
963
964
965
966
967
968
    """Format one line for physical device status.

    @type dtype: str
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
    @type status: tuple
    @param status: a tuple as returned from L{backend.FindBlockDevice}
969
    @return: the string representing the status
970
971

    """
Iustin Pop's avatar
Iustin Pop committed
972
    if not status:
973
974
      return "not active"
    txt = ""
975
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
976
977
    if major is None:
      major_string = "N/A"
Iustin Pop's avatar
Iustin Pop committed
978
    else:
979
      major_string = str(compat.TryToRoman(major, convert=roman))
980

981
982
983
    if minor is None:
      minor_string = "N/A"
    else:
984
      minor_string = str(compat.TryToRoman(minor, convert=roman))
985
986
987
988
989
990

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

  # the header
  if top_level:
    if dev["iv_name"] is not None:
      txt = dev["iv_name"]
    else:
1020
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1021
  else:
1022
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1023
1024
1025
1026
1027
  if isinstance(dev["size"], int):
    nice_size = utils.FormatUnit(dev["size"], "h")
  else:
    nice_size = dev["size"]
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1028
1029
1030
  data = []
  if top_level:
    data.append(("access mode", dev["mode"]))
Iustin Pop's avatar
Iustin Pop committed
1031
  if dev["logical_id"] is not None:
1032
    try:
1033
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1034
1035
1036
1037
1038
1039
    except ValueError:
      l_id = [str(dev["logical_id"])]
    if len(l_id) == 1:
      data.append(("logical_id", l_id[0]))
    else:
      data.extend(l_id)
Iustin Pop's avatar
Iustin Pop committed
1040
  elif dev["physical_id"] is not None:
1041
1042
    data.append("physical_id:")
    data.append([dev["physical_id"]])
1043
  if not static:
1044
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))