gnt_instance.py 52.3 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
    # Validate special fields
    if spec['primary_node'] is not None:
306
      if (spec['template'] in constants.DTS_INT_MIRROR and
307
308
          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
    tmp_nics = []
360
    for field in constants.INIC_PARAMS:
361
362
363
364
365
366
367
      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
758
759
760
761
762
763
  iallocator = opts.iallocator
  target_node = opts.dst_node

  if iallocator and target_node:
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
                               " node (-n) but not both", errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
764

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

768
769
770
771
772
    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
773

774
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
775
                                  ignore_consistency=opts.ignore_consistency,
776
777
778
                                  shutdown_timeout=opts.shutdown_timeout,
                                  iallocator=iallocator,
                                  target_node=target_node)
779
  SubmitOrSend(op, opts, cl=cl)
780
  return 0
Iustin Pop's avatar
Iustin Pop committed
781
782


783
784
785
786
787
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
788
789
790
791
792
  @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
793
794

  """
795
  cl = GetClient()
796
797
  instance_name = args[0]
  force = opts.force
798
799
800
801
802
803
  iallocator = opts.iallocator
  target_node = opts.dst_node

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

  if not force:
806
807
    _EnsureInstancesExist(cl, [instance_name])

808
809
810
811
812
813
814
    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
815
816
    usertext += (" might impact the instance if anything goes wrong"
                 " (e.g. due to bugs in the hypervisor). Continue?")
817
818
819
    if not AskUser(usertext):
      return 1

820
  # this should be removed once --non-live is deprecated
821
  if not opts.live and opts.migration_mode is not None:
822
    raise errors.OpPrereqError("Only one of the --non-live and "
823
                               "--migration-mode options can be passed",
824
825
                               errors.ECODE_INVAL)
  if not opts.live: # --non-live passed
826
    mode = constants.HT_MIGRATION_NONLIVE
827
  else:
828
    mode = opts.migration_mode
829

830
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
831
832
                                 cleanup=opts.cleanup, iallocator=iallocator,
                                 target_node=target_node)
833
  SubmitOpCode(op, cl=cl, opts=opts)
834
835
836
  return 0


Iustin Pop's avatar
Iustin Pop committed
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
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

858
  op = opcodes.OpInstanceMove(instance_name=instance_name,
859
                              target_node=opts.node,
860
                              shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
861
862
863
864
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
865
866
867
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

868
869
870
871
872
  @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
873
874
875
876

  """
  instance_name = args[0]

877
  op = opcodes.OpInstanceConsole(instance_name=instance_name)
878

879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
  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):
895
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
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
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935

  @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)
936
  else:
937
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
938
939

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
940
941


942
def _FormatLogicalID(dev_type, logical_id, roman):
943
944
945
946
947
948
  """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 = [
949
950
951
952
953
      ("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)),
954
955
956
957
958
959
960
961
962
963
964
      ("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


965
def _FormatBlockDevInfo(idx, top_level, dev, static, roman):
Iustin Pop's avatar
Iustin Pop committed
966
967
  """Show block device information.

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

971
972
973
974
  @type idx: int
  @param idx: the index of the current disk
  @type top_level: boolean
  @param top_level: if this a top-level disk?
975
976
977
978
979
  @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
980
981
  @type roman: boolean
  @param roman: whether to try to use roman integers
982
983
  @return: a list of either strings, tuples or lists
      (which should be formatted at a higher indent level)
984

Iustin Pop's avatar
Iustin Pop committed
985
  """
986
  def helper(dtype, status):
987
988
989
990
991
992
    """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}
993
    @return: the string representing the status
994
995

    """
Iustin Pop's avatar
Iustin Pop committed
996
    if not status:
997
998
      return "not active"
    txt = ""
999
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
1000
1001
    if major is None:
      major_string = "N/A"
Iustin Pop's avatar
Iustin Pop committed
1002
    else:
1003
      major_string = str(compat.TryToRoman(major, convert=roman))
1004

1005
1006
1007
    if minor is None:
      minor_string = "N/A"
    else:
1008
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1009
1010
1011
1012
1013
1014

    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:
1015
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1016
        else:
1017
1018
1019
1020
1021
1022
1023
          sync_text += " ETA unknown"
      else:
        sync_text = "in sync"
      if degr:
        degr_text = "*DEGRADED*"
      else:
        degr_text = "ok"
1024
      if ldisk_status == constants.LDS_FAULTY:
1025
        ldisk_text = " *MISSING DISK*"
1026
1027
      elif ldisk_status == constants.LDS_UNKNOWN:
        ldisk_text = " *UNCERTAIN STATE*"
1028
1029
1030
1031
      else:
        ldisk_text = ""
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
    elif dtype == constants.LD_LV:
1032
      if ldisk_status == constants.LDS_FAULTY:
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
        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:
1044
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)