gnt_instance.py 51.6 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
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:
182
183
184
      if opts.multi_mode == _SHUTDOWN_CLUSTER:
        ToStdout("Cluster is empty, no instances to shutdown")
        return 0
185
      raise errors.OpPrereqError("Selection filter does not match"
186
                                 " any instances", errors.ECODE_INVAL)
187
188
    multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
    if not (opts.force_multi or not multi_on
189
            or ConfirmOperation(inames, "instances", operation)):
190
      return 1
191
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
192
193
194
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
Iustin Pop's avatar
Iustin Pop committed
195
196
197
    results = jex.WaitOrShow(not opts.submit_only)
    rcode = compat.all(row[0] for row in results)
    return int(not rcode)
198
199
200
  return realfn


Iustin Pop's avatar
Iustin Pop committed
201
def ListInstances(opts, args):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
202
  """List instances and their properties.
Iustin Pop's avatar
Iustin Pop committed
203

204
205
206
207
208
209
  @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
210
  """
211
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
Iustin Pop's avatar
Iustin Pop committed
212

213
214
215
216
217
218
  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
219

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


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
237
238
239
240
241


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

Iustin Pop's avatar
Iustin Pop committed
242
  This is just a wrapper over GenericInstanceCreate.
Iustin Pop's avatar
Iustin Pop committed
243
244

  """
Iustin Pop's avatar
Iustin Pop committed
245
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
Iustin Pop's avatar
Iustin Pop committed
246
247


248
def BatchCreate(opts, args):
249
250
251
252
253
254
  """Create instances using a definition file.

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

    {"instance-name":{
255
      "disk_size": [20480],
256
257
258
259
      "template": "drbd",
      "backend": {
        "memory": 512,
        "vcpus": 1 },
260
      "os": "debootstrap",
261
262
263
264
265
266
267
268
269
270
271
272
273
      "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
274
275

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

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

  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.' %
303
                                   required_field, errors.ECODE_INVAL)
304
305
306
307
308
    # 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'
309
310
                                   ' there was no secondary provided.',
                                   errors.ECODE_INVAL)
311
312
    elif spec['iallocator'] is None:
      raise errors.OpPrereqError('You have to provide at least a primary_node'
313
314
                                 ' or an iallocator.',
                                 errors.ECODE_INVAL)
315

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

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

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

332
  jex = JobExecutor(opts=opts)
333

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

Iustin Pop's avatar
Iustin Pop committed
343
344
    hypervisor = specs['hypervisor']
    hvparams = specs['hvparams']
345

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

356
357
358
    utils.ForceDictType(specs['backend'], constants.BES_PARAMETER_TYPES)
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

359
360
361
362
363
364
365
366
367
    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"
368
369
                                 " individual nic fields as well",
                                 errors.ECODE_INVAL)
370
371
372
373
374
    elif specs['nics'] is not None:
      tmp_nics = specs['nics']
    elif not tmp_nics:
      tmp_nics = [{}]

375
    op = opcodes.OpInstanceCreate(instance_name=name,
376
                                  disks=disks,
377
378
379
                                  disk_template=specs['template'],
                                  mode=constants.INSTANCE_CREATE,
                                  os_type=specs['os'],
380
                                  force_variant=specs["force_variant"],
381
382
                                  pnode=specs['primary_node'],
                                  snode=specs['secondary_node'],
383
                                  nics=tmp_nics,
384
385
                                  start=specs['start'],
                                  ip_check=specs['ip_check'],
386
                                  name_check=specs['name_check'],
387
388
389
390
391
392
393
394
                                  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'])

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

  return 0


402
403
404
def ReinstallInstance(opts, args):
  """Reinstall an instance.

405
406
407
408
409
410
  @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
411
412

  """
413
414
415
416
417
418
  # 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:
419
420
    raise errors.OpPrereqError("Selection filter does not match any instances",
                               errors.ECODE_INVAL)
421

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

    if not result:
428
      ToStdout("Can't get the OS list")
429
430
      return 1

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

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

    if selected == 'exit':
445
      ToStderr("User aborted reinstall, exiting")
446
447
      return 1

448
    os_name = selected
449
    os_msg = "change the OS to '%s'" % selected
450
  else:
451
    os_name = opts.os
452
453
454
455
    if opts.os is not None:
      os_msg = "change the OS to '%s'" % os_name
    else:
      os_msg = "keep the same OS"
456

457
458
459
  # third, get confirmation: multi-reinstall requires --force-multi,
  # single-reinstall either --force or --force-multi (--force-multi is
  # a stronger --force)
460
461
  multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
  if multi_on:
462
463
    warn_msg = ("Note: this will remove *all* data for the"
                " below instances! It will %s.\n" % os_msg)
464
    if not (opts.force_multi or
465
            ConfirmOperation(inames, "instances", "reinstall", extra=warn_msg)):
466
      return 1
467
  else:
468
    if not (opts.force or opts.force_multi):
469
470
      usertext = ("This will reinstall the instance '%s' (and %s) which"
                  " removes all data. Continue?") % (inames[0], os_msg)
471
472
473
      if not AskUser(usertext):
        return 1

474
  jex = JobExecutor(verbose=multi_on, opts=opts)
475
  for instance_name in inames:
476
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
477
                                     os_type=os_name,
478
479
                                     force_variant=opts.force_variant,
                                     osparams=opts.osparams)
480
    jex.QueueJob(instance_name, op)
481

482
  jex.WaitOrShow(not opts.submit_only)
483
484
485
  return 0


Iustin Pop's avatar
Iustin Pop committed
486
487
488
def RemoveInstance(opts, args):
  """Remove an instance.

489
490
491
492
493
494
  @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
495
496
497
498

  """
  instance_name = args[0]
  force = opts.force
499
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
500
501

  if not force:
502
503
    _EnsureInstancesExist(cl, [instance_name])

Iustin Pop's avatar
Iustin Pop committed
504
505
506
    usertext = ("This will remove the volumes of the instance %s"
                " (including mirrors), thus removing all the data"
                " of the instance. Continue?") % instance_name
507
    if not AskUser(usertext):
Iustin Pop's avatar
Iustin Pop committed
508
509
      return 1

510
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
511
                                ignore_failures=opts.ignore_failures,
512
                                shutdown_timeout=opts.shutdown_timeout)
513
  SubmitOrSend(op, opts, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
514
515
516
  return 0


517
def RenameInstance(opts, args):
Guido Trotter's avatar
Guido Trotter committed
518
  """Rename an instance.
519

520
521
522
523
524
525
  @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
526
527

  """
528
  if not opts.name_check:
529
530
531
532
    if not AskUser("As you disabled the check of the DNS entry, please verify"
                   " that '%s' is a FQDN. Continue?" % args[1]):
      return 1

533
  op = opcodes.OpInstanceRename(instance_name=args[0],
534
                                new_name=args[1],
535
536
                                ip_check=opts.ip_check,
                                name_check=opts.name_check)
537
538
  result = SubmitOrSend(op, opts)

539
540
  if result:
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
541

542
543
544
  return 0


Iustin Pop's avatar
Iustin Pop committed
545
546
547
548
def ActivateDisks(opts, args):
  """Activate an instance's disks.

  This serves two purposes:
549
550
    - 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
551
552
    - it repairs inactive secondary drbds

553
554
555
556
557
558
  @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
559
560
  """
  instance_name = args[0]
561
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
562
                                       ignore_size=opts.ignore_size)
563
  disks_info = SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
564
  for host, iname, nname in disks_info:
565
    ToStdout("%s:%s:%s", host, iname, nname)
Iustin Pop's avatar
Iustin Pop committed
566
567
568
569
  return 0


def DeactivateDisks(opts, args):
Iustin Pop's avatar
Iustin Pop committed
570
  """Deactivate an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
571
572
573
574

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

575
576
577
578
579
580
  @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
581
582
  """
  instance_name = args[0]
583
584
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
                                         force=opts.force)
585
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
586
587
588
  return 0


Iustin Pop's avatar
Iustin Pop committed
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
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 = []

609
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
Iustin Pop's avatar
Iustin Pop committed
610
611
612
613
614
                                       disks=opts.disks)
  SubmitOrSend(op, opts)
  return 0


Iustin Pop's avatar
Iustin Pop committed
615
def GrowDisk(opts, args):
616
  """Grow an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
617

618
619
620
621
622
623
  @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
624
625
626
627

  """
  instance = args[0]
  disk = args[1]
628
629
  try:
    disk = int(disk)
630
  except (TypeError, ValueError), err:
631
632
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
                               errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
633
  amount = utils.ParseUnit(args[2])
Iustin Pop's avatar
Iustin Pop committed
634
635
636
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
                                  disk=disk, amount=amount,
                                  wait_for_sync=opts.wait_for_sync)
637
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
638
639
640
  return 0


641
def _StartupInstance(name, opts):
642
  """Startup instances.
Iustin Pop's avatar
Iustin Pop committed
643

644
645
  This returns the opcode to start an instance, and its decorator will
  wrap this into a loop starting all desired instances.
646

647
  @param name: the name of the instance to act on
648
  @param opts: the command line options selected by the user
649
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
650
651

  """
652
  op = opcodes.OpInstanceStartup(instance_name=name,
653
654
                                 force=opts.force,
                                 ignore_offline_nodes=opts.ignore_offline)
655
656
657
658
659
660
  # 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
661

662

663
def _RebootInstance(name, opts):
664
665
  """Reboot instance(s).

666
667
  This returns the opcode to reboot an instance, and its decorator
  will wrap this into a loop rebooting all desired instances.
668

669
  @param name: the name of the instance to act on
670
  @param opts: the command line options selected by the user
671
  @return: the opcode needed for the operation
672
673

  """
674
  return opcodes.OpInstanceReboot(instance_name=name,
675
                                  reboot_type=opts.reboot_type,
676
                                  ignore_secondaries=opts.ignore_secondaries,
677
                                  shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
678

679

680
def _ShutdownInstance(name, opts):
Iustin Pop's avatar
Iustin Pop committed
681
682
  """Shutdown an instance.

683
684
685
686
  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
687
  @param opts: the command line options selected by the user
688
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
689
690

  """
691
  return opcodes.OpInstanceShutdown(instance_name=name,
692
693
                                    timeout=opts.timeout,
                                    ignore_offline_nodes=opts.ignore_offline)
Iustin Pop's avatar
Iustin Pop committed
694
695
696
697
698


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

699
700
701
702
703
  @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
704
705

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

734
735
736
737
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
                                      remote_node=new_2ndary, mode=mode,
                                      iallocator=iallocator,
                                      early_release=opts.early_release)
738
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
739
740
741
742
743
744
745
746
747
  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.

748
749
750
751
752
  @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
753
754

  """
755
  cl = GetClient()
756
757
  instance_name = args[0]
  force = opts.force
Iustin Pop's avatar
Iustin Pop committed
758

759
  if not force:
760
761
    _EnsureInstancesExist(cl, [instance_name])

762
763
764
765
766
    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
767

768
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
769
                                  ignore_consistency=opts.ignore_consistency,
770
                                  shutdown_timeout=opts.shutdown_timeout)
771
  SubmitOrSend(op, opts, cl=cl)
772
  return 0
Iustin Pop's avatar
Iustin Pop committed
773
774


775
776
777
778
779
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
780
781
782
783
784
  @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
785
786

  """
787
  cl = GetClient()
788
789
790
791
  instance_name = args[0]
  force = opts.force

  if not force:
792
793
    _EnsureInstancesExist(cl, [instance_name])

794
795
796
797
798
799
800
    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
801
802
    usertext += (" might impact the instance if anything goes wrong"
                 " (e.g. due to bugs in the hypervisor). Continue?")
803
804
805
    if not AskUser(usertext):
      return 1

806
  # this should be removed once --non-live is deprecated
807
  if not opts.live and opts.migration_mode is not None:
808
    raise errors.OpPrereqError("Only one of the --non-live and "
809
                               "--migration-mode options can be passed",
810
811
                               errors.ECODE_INVAL)
  if not opts.live: # --non-live passed
812
    mode = constants.HT_MIGRATION_NONLIVE
813
  else:
814
    mode = opts.migration_mode
815

816
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
817
                                 cleanup=opts.cleanup)
818
  SubmitOpCode(op, cl=cl, opts=opts)
819
820
821
  return 0


Iustin Pop's avatar
Iustin Pop committed
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
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

843
  op = opcodes.OpInstanceMove(instance_name=instance_name,
844
                              target_node=opts.node,
845
                              shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
846
847
848
849
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
850
851
852
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

853
854
855
856
857
  @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
858
859
860
861

  """
  instance_name = args[0]

862
  op = opcodes.OpInstanceConsole(instance_name=instance_name)
863

864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
  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):
880
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
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
912
913
914
915
916
917
918
919
920

  @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)
921
  else:
922
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
923
924

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
925
926


927
def _FormatLogicalID(dev_type, logical_id, roman):
928
929
930
931
932
933
  """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 = [
934
935
936
937
938
      ("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)),
939
940
941
942
943
944
945
946
947
948
949
      ("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


950
def _FormatBlockDevInfo(idx, top_level, dev, static, roman):
Iustin Pop's avatar
Iustin Pop committed
951
952
  """Show block device information.

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

956
957
958
959
  @type idx: int
  @param idx: the index of the current disk
  @type top_level: boolean
  @param top_level: if this a top-level disk?
960
961
962
963
964
  @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
965
966
  @type roman: boolean
  @param roman: whether to try to use roman integers
967
968
  @return: a list of either strings, tuples or lists
      (which should be formatted at a higher indent level)
969

Iustin Pop's avatar
Iustin Pop committed
970
  """
971
  def helper(dtype, status):
972
973
974
975
976
977
    """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}
978
    @return: the string representing the status
979
980

    """
Iustin Pop's avatar
Iustin Pop committed
981
    if not status:
982
983
      return "not active"
    txt = ""
984
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
985
986
    if major is None:
      major_string = "N/A"
Iustin Pop's avatar
Iustin Pop committed
987
    else:
988
      major_string = str(compat.TryToRoman(major, convert=roman))
989

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

    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:
1000
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1001
        else:
1002
1003
1004
1005
1006
1007
1008
          sync_text += " ETA unknown"
      else:
        sync_text = "in sync"
      if degr:
        degr_text = "*DEGRADED*"
      else:
        degr_text = "ok"
1009
      if ldisk_status == constants.LDS_FAULTY:
1010
        ldisk_text = " *MISSING DISK*"
1011
1012
      elif ldisk_status == constants.LDS_UNKNOWN:
        ldisk_text = " *UNCERTAIN STATE*"
1013
1014
1015
1016
      else:
        ldisk_text = ""
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
    elif dtype == constants.LD_LV:
1017
      if ldisk_status == constants.LDS_FAULTY:
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
        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:
1029
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1030
  else:
1031
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1032
1033
1034
1035
1036
  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)]
1037
1038
1039
  data = []
  if top_level:
    data.append(("access mode", dev["mode"]))
Iustin Pop's avatar
Iustin Pop committed
1040
  if dev["logical_id"] is not None:
1041
    try:
1042
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)