gnt_instance.py 52.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
61
_VALUE_TRUE = "true"

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

67

68
def _ExpandMultiNames(mode, names, client=None):
69
70
71
72
  """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
73
  primary/secondary will be returned. For _SHUTDOWN_NODES_BOTH, all
74
75
76
77
  instances having those nodes as either primary or secondary will be
  returned. For _SHUTDOWN_INSTANCES, the given instances will be
  returned.

78
79
80
81
82
83
84
85
86
87
88
89
  @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

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

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

  elif mode in (_SHUTDOWN_NODES_BOTH,
                _SHUTDOWN_NODES_PRI,
104
105
106
107
108
109
110
111
112
113
114
                _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"],
115
                              False)
116

117
118
119
120
    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))
121
    if mode in (_SHUTDOWN_NODES_BOTH, _SHUTDOWN_NODES_BOTH_BY_TAGS):
122
      inames = pri_names + sec_names
123
    elif mode in (_SHUTDOWN_NODES_PRI, _SHUTDOWN_NODES_PRI_BY_TAGS):
124
      inames = pri_names
125
    elif mode in (_SHUTDOWN_NODES_SEC, _SHUTDOWN_NODES_SEC_BY_TAGS):
126
127
128
129
130
      inames = sec_names
    else:
      raise errors.ProgrammerError("Unhandled shutdown type")
  elif mode == _SHUTDOWN_INSTANCES:
    if not names:
131
132
      raise errors.OpPrereqError("No instance names passed",
                                 errors.ECODE_INVAL)
133
    idata = client.QueryInstances(names, ["name"], False)
134
    inames = [row[0] for row in idata]
135
136
137
138
139
140
  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)]
141
  else:
142
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
143
144

  return inames
Iustin Pop's avatar
Iustin Pop committed
145
146


147
def _ConfirmOperation(inames, text, extra=""):
148
149
150
151
152
  """Ask the user to confirm an operation on a list of instances.

  This function is used to request confirmation for doing an operation
  on a given list of instances.

153
154
155
156
157
158
159
160
  @type inames: list
  @param inames: the list of names that we display when
      we ask for confirmation
  @type text: str
  @param text: the operation that the user should confirm
      (e.g. I{shutdown} or I{startup})
  @rtype: boolean
  @return: True or False depending on user's confirmation.
161
162
163

  """
  count = len(inames)
164
165
  msg = ("The %s will operate on %d instances.\n%s"
         "Do you want to continue?" % (text, count, extra))
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
  affected = ("\nAffected instances:\n" +
              "\n".join(["  %s" % name for name in inames]))

  choices = [('y', True, 'Yes, execute the %s' % text),
             ('n', False, 'No, abort the %s' % text)]

  if count > 20:
    choices.insert(1, ('v', 'v', 'View the list of affected instances'))
    ask = msg
  else:
    ask = msg + affected

  choice = AskUser(ask, choices)
  if choice == 'v':
    choices.pop(1)
181
    choice = AskUser(msg + affected, choices)
182
183
184
  return choice


185
186
187
188
189
190
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
191
  @type client: L{ganeti.luxi.Client}
192
193
194
195
196
197
  @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

  """
198
  # TODO: change LUInstanceQuery to that it actually returns None
199
  # instead of raising an exception, or devise a better mechanism
200
  result = client.QueryInstances(names, ["name"], False)
201
202
  for orig_name, row in zip(names, result):
    if row[0] is None:
203
204
      raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
                                 errors.ECODE_NOENT)
205
206


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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"
223
                                 " any instances", errors.ECODE_INVAL)
224
225
226
227
    multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1
    if not (opts.force_multi or not multi_on
            or _ConfirmOperation(inames, operation)):
      return 1
228
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
229
230
231
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
Iustin Pop's avatar
Iustin Pop committed
232
233
234
    results = jex.WaitOrShow(not opts.submit_only)
    rcode = compat.all(row[0] for row in results)
    return int(not rcode)
235
236
237
  return realfn


Iustin Pop's avatar
Iustin Pop committed
238
def ListInstances(opts, args):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
239
  """List instances and their properties.
Iustin Pop's avatar
Iustin Pop committed
240

241
242
243
244
245
246
  @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
247
  """
248
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
Iustin Pop's avatar
Iustin Pop committed
249

250
251
252
253
254
255
  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
256

257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
  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
274
275
276
277
278


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

Iustin Pop's avatar
Iustin Pop committed
279
  This is just a wrapper over GenericInstanceCreate.
Iustin Pop's avatar
Iustin Pop committed
280
281

  """
Iustin Pop's avatar
Iustin Pop committed
282
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
Iustin Pop's avatar
Iustin Pop committed
283
284


285
def BatchCreate(opts, args):
286
287
288
289
290
291
  """Create instances using a definition file.

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

    {"instance-name":{
292
      "disk_size": [20480],
293
294
295
296
      "template": "drbd",
      "backend": {
        "memory": 512,
        "vcpus": 1 },
297
      "os": "debootstrap",
298
299
300
301
302
303
304
305
306
307
308
309
310
      "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
311
312

  """
313
  _DEFAULT_SPECS = {"disk_size": [20 * 1024],
314
315
316
317
                    "backend": {},
                    "iallocator": None,
                    "primary_node": None,
                    "secondary_node": None,
318
                    "nics": None,
319
320
                    "start": True,
                    "ip_check": True,
321
                    "name_check": True,
322
                    "hypervisor": None,
Iustin Pop's avatar
Iustin Pop committed
323
                    "hvparams": {},
324
                    "file_storage_dir": None,
325
                    "force_variant": False,
326
327
328
329
                    "file_driver": 'loop'}

  def _PopulateWithDefaults(spec):
    """Returns a new hash combined with default values."""
330
331
332
    mydict = _DEFAULT_SPECS.copy()
    mydict.update(spec)
    return mydict
333
334
335
336
337
338
339

  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.' %
340
                                   required_field, errors.ECODE_INVAL)
341
342
343
344
345
    # 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'
346
347
                                   ' there was no secondary provided.',
                                   errors.ECODE_INVAL)
348
349
    elif spec['iallocator'] is None:
      raise errors.OpPrereqError('You have to provide at least a primary_node'
350
351
                                 ' or an iallocator.',
                                 errors.ECODE_INVAL)
352

Iustin Pop's avatar
Iustin Pop committed
353
354
    if (spec['hvparams'] and
        not isinstance(spec['hvparams'], dict)):
355
356
      raise errors.OpPrereqError('Hypervisor parameters must be a dict.',
                                 errors.ECODE_INVAL)
357
358
359

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

Guido Trotter's avatar
Guido Trotter committed
365
366
367
368
  if not isinstance(instance_data, dict):
    ToStderr("The instance definition file is not in dict format.")
    return 1

369
  jex = JobExecutor(opts=opts)
370

371
372
373
  # Iterate over the instances and do:
  #  * Populate the specs with default value
  #  * Validate the instance specs
Guido Trotter's avatar
Guido Trotter committed
374
  i_names = utils.NiceSort(instance_data.keys()) # pylint: disable-msg=E1103
375
376
  for name in i_names:
    specs = instance_data[name]
377
378
379
    specs = _PopulateWithDefaults(specs)
    _Validate(specs)

Iustin Pop's avatar
Iustin Pop committed
380
381
    hypervisor = specs['hypervisor']
    hvparams = specs['hvparams']
382

383
384
385
386
    disks = []
    for elem in specs['disk_size']:
      try:
        size = utils.ParseUnit(elem)
387
      except (TypeError, ValueError), err:
388
389
        raise errors.OpPrereqError("Invalid disk size '%s' for"
                                   " instance %s: %s" %
390
                                   (elem, name, err), errors.ECODE_INVAL)
391
392
      disks.append({"size": size})

393
394
395
    utils.ForceDictType(specs['backend'], constants.BES_PARAMETER_TYPES)
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

396
397
398
399
400
401
402
403
404
    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"
405
406
                                 " individual nic fields as well",
                                 errors.ECODE_INVAL)
407
408
409
410
411
    elif specs['nics'] is not None:
      tmp_nics = specs['nics']
    elif not tmp_nics:
      tmp_nics = [{}]

412
    op = opcodes.OpInstanceCreate(instance_name=name,
413
                                  disks=disks,
414
415
416
                                  disk_template=specs['template'],
                                  mode=constants.INSTANCE_CREATE,
                                  os_type=specs['os'],
417
                                  force_variant=specs["force_variant"],
418
419
                                  pnode=specs['primary_node'],
                                  snode=specs['secondary_node'],
420
                                  nics=tmp_nics,
421
422
                                  start=specs['start'],
                                  ip_check=specs['ip_check'],
423
                                  name_check=specs['name_check'],
424
425
426
427
428
429
430
431
                                  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'])

432
433
434
    jex.QueueJob(name, op)
  # we never want to wait, just show the submitted job IDs
  jex.WaitOrShow(False)
435
436
437
438

  return 0


439
440
441
def ReinstallInstance(opts, args):
  """Reinstall an instance.

442
443
444
445
446
447
  @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
448
449

  """
450
451
452
453
454
455
  # 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:
456
457
    raise errors.OpPrereqError("Selection filter does not match any instances",
                               errors.ECODE_INVAL)
458

459
  # second, if requested, ask for an OS
460
  if opts.select_os is True:
461
    op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
462
    result = SubmitOpCode(op, opts=opts)
463
464

    if not result:
465
      ToStdout("Can't get the OS list")
466
467
      return 1

468
    ToStdout("Available OS templates:")
469
470
    number = 0
    choices = []
471
472
473
474
475
    for (name, variants) in result:
      for entry in CalculateOSNames(name, variants):
        ToStdout("%3s: %s", number, entry)
        choices.append(("%s" % number, entry, entry))
        number += 1
476
477

    choices.append(('x', 'exit', 'Exit gnt-instance reinstall'))
478
    selected = AskUser("Enter OS template number (or x to abort):",
479
480
481
                       choices)

    if selected == 'exit':
482
      ToStderr("User aborted reinstall, exiting")
483
484
      return 1

485
    os_name = selected
486
  else:
487
    os_name = opts.os
488

489
490
491
  # third, get confirmation: multi-reinstall requires --force-multi,
  # single-reinstall either --force or --force-multi (--force-multi is
  # a stronger --force)
492
493
494
  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"
495
    if not (opts.force_multi or
496
            _ConfirmOperation(inames, "reinstall", extra=warn_msg)):
497
      return 1
498
  else:
499
    if not (opts.force or opts.force_multi):
500
      usertext = ("This will reinstall the instance %s and remove"
Iustin Pop's avatar
Iustin Pop committed
501
                  " all data. Continue?") % inames[0]
502
503
504
      if not AskUser(usertext):
        return 1

505
  jex = JobExecutor(verbose=multi_on, opts=opts)
506
  for instance_name in inames:
507
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
508
                                     os_type=os_name,
509
510
                                     force_variant=opts.force_variant,
                                     osparams=opts.osparams)
511
    jex.QueueJob(instance_name, op)
512

513
  jex.WaitOrShow(not opts.submit_only)
514
515
516
  return 0


Iustin Pop's avatar
Iustin Pop committed
517
518
519
def RemoveInstance(opts, args):
  """Remove an instance.

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

  """
  instance_name = args[0]
  force = opts.force
530
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
531
532

  if not force:
533
534
    _EnsureInstancesExist(cl, [instance_name])

Iustin Pop's avatar
Iustin Pop committed
535
536
537
    usertext = ("This will remove the volumes of the instance %s"
                " (including mirrors), thus removing all the data"
                " of the instance. Continue?") % instance_name
538
    if not AskUser(usertext):
Iustin Pop's avatar
Iustin Pop committed
539
540
      return 1

Iustin Pop's avatar
Iustin Pop committed
541
  op = opcodes.OpRemoveInstance(instance_name=instance_name,
542
                                ignore_failures=opts.ignore_failures,
543
                                shutdown_timeout=opts.shutdown_timeout)
544
  SubmitOrSend(op, opts, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
545
546
547
  return 0


548
def RenameInstance(opts, args):
Guido Trotter's avatar
Guido Trotter committed
549
  """Rename an instance.
550

551
552
553
554
555
556
  @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
557
558

  """
559
  if not opts.name_check:
560
561
562
563
    if not AskUser("As you disabled the check of the DNS entry, please verify"
                   " that '%s' is a FQDN. Continue?" % args[1]):
      return 1

564
565
  op = opcodes.OpRenameInstance(instance_name=args[0],
                                new_name=args[1],
566
567
                                ip_check=opts.ip_check,
                                name_check=opts.name_check)
568
569
  result = SubmitOrSend(op, opts)

570
571
  if result:
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
572

573
574
575
  return 0


Iustin Pop's avatar
Iustin Pop committed
576
577
578
579
def ActivateDisks(opts, args):
  """Activate an instance's disks.

  This serves two purposes:
580
581
    - 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
582
583
    - it repairs inactive secondary drbds

584
585
586
587
588
589
  @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
590
591
  """
  instance_name = args[0]
592
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
593
                                       ignore_size=opts.ignore_size)
594
  disks_info = SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
595
  for host, iname, nname in disks_info:
596
    ToStdout("%s:%s:%s", host, iname, nname)
Iustin Pop's avatar
Iustin Pop committed
597
598
599
600
  return 0


def DeactivateDisks(opts, args):
Iustin Pop's avatar
Iustin Pop committed
601
  """Deactivate an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
602
603
604
605

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

606
607
608
609
610
611
  @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
612
613
  """
  instance_name = args[0]
614
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
615
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
616
617
618
  return 0


Iustin Pop's avatar
Iustin Pop committed
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
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 = []

639
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
Iustin Pop's avatar
Iustin Pop committed
640
641
642
643
644
                                       disks=opts.disks)
  SubmitOrSend(op, opts)
  return 0


Iustin Pop's avatar
Iustin Pop committed
645
def GrowDisk(opts, args):
646
  """Grow an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
647

648
649
650
651
652
653
  @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
654
655
656
657

  """
  instance = args[0]
  disk = args[1]
658
659
  try:
    disk = int(disk)
660
  except (TypeError, ValueError), err:
661
662
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
                               errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
663
  amount = utils.ParseUnit(args[2])
Iustin Pop's avatar
Iustin Pop committed
664
665
666
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
                                  disk=disk, amount=amount,
                                  wait_for_sync=opts.wait_for_sync)
667
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
668
669
670
  return 0


671
def _StartupInstance(name, opts):
672
  """Startup instances.
Iustin Pop's avatar
Iustin Pop committed
673

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

677
  @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
  op = opcodes.OpStartupInstance(instance_name=name,
683
684
                                 force=opts.force,
                                 ignore_offline_nodes=opts.ignore_offline)
685
686
687
688
689
690
  # 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
691

692

693
def _RebootInstance(name, opts):
694
695
  """Reboot instance(s).

696
697
  This returns the opcode to reboot an instance, and its decorator
  will wrap this into a loop rebooting all desired instances.
698

699
  @param name: the name of the instance to act on
700
  @param opts: the command line options selected by the user
701
  @return: the opcode needed for the operation
702
703

  """
704
  return opcodes.OpInstanceReboot(instance_name=name,
705
                                  reboot_type=opts.reboot_type,
706
                                  ignore_secondaries=opts.ignore_secondaries,
707
                                  shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
708

709

710
def _ShutdownInstance(name, opts):
Iustin Pop's avatar
Iustin Pop committed
711
712
  """Shutdown an instance.

713
714
715
716
  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
717
  @param opts: the command line options selected by the user
718
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
719
720

  """
721
  return opcodes.OpShutdownInstance(instance_name=name,
722
723
                                    timeout=opts.timeout,
                                    ignore_offline_nodes=opts.ignore_offline)
Iustin Pop's avatar
Iustin Pop committed
724
725
726
727
728


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

729
730
731
732
733
  @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
734
735

  """
736
  new_2ndary = opts.dst_node
737
  iallocator = opts.iallocator
738
  if opts.disks is None:
739
    disks = []
740
  else:
741
742
    try:
      disks = [int(i) for i in opts.disks.split(",")]
743
    except (TypeError, ValueError), err:
744
745
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
                                 errors.ECODE_INVAL)
746
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
747
748
         new_2ndary is not None, iallocator is not None].count(True)
  if cnt != 1:
749
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -i"
750
                               " options must be passed", errors.ECODE_INVAL)
751
  elif opts.on_primary:
752
    mode = constants.REPLACE_DISK_PRI
753
  elif opts.on_secondary:
754
    mode = constants.REPLACE_DISK_SEC
755
756
757
758
  elif opts.auto:
    mode = constants.REPLACE_DISK_AUTO
    if disks:
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
759
                                 " mode", errors.ECODE_INVAL)
760
761
762
  elif new_2ndary is not None or iallocator is not None:
    # replace secondary
    mode = constants.REPLACE_DISK_CHG
763
764

  op = opcodes.OpReplaceDisks(instance_name=args[0], disks=disks,
765
                              remote_node=new_2ndary, mode=mode,
766
767
                              iallocator=iallocator,
                              early_release=opts.early_release)
768
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
769
770
771
772
773
774
775
776
777
  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.

778
779
780
781
782
  @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
783
784

  """
785
  cl = GetClient()
786
787
  instance_name = args[0]
  force = opts.force
Iustin Pop's avatar
Iustin Pop committed
788

789
  if not force:
790
791
    _EnsureInstancesExist(cl, [instance_name])

792
793
794
795
796
    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
797

798
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
799
                                  ignore_consistency=opts.ignore_consistency,
800
                                  shutdown_timeout=opts.shutdown_timeout)
801
  SubmitOrSend(op, opts, cl=cl)
802
  return 0
Iustin Pop's avatar
Iustin Pop committed
803
804


805
806
807
808
809
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
810
811
812
813
814
  @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
815
816

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

  if not force:
822
823
    _EnsureInstancesExist(cl, [instance_name])

824
825
826
827
828
829
830
    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
831
832
    usertext += (" might impact the instance if anything goes wrong"
                 " (e.g. due to bugs in the hypervisor). Continue?")
833
834
835
    if not AskUser(usertext):
      return 1

836
  # this should be removed once --non-live is deprecated
837
  if not opts.live and opts.migration_mode is not None:
838
    raise errors.OpPrereqError("Only one of the --non-live and "
839
                               "--migration-mode options can be passed",
840
841
                               errors.ECODE_INVAL)
  if not opts.live: # --non-live passed
842
    mode = constants.HT_MIGRATION_NONLIVE
843
  else:
844
    mode = opts.migration_mode
845

846
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
847
                                 cleanup=opts.cleanup)
848
  SubmitOpCode(op, cl=cl, opts=opts)
849
850
851
  return 0


Iustin Pop's avatar
Iustin Pop committed
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
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

873
  op = opcodes.OpInstanceMove(instance_name=instance_name,
874
                              target_node=opts.node,
875
                              shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
876
877
878
879
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
880
881
882
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

883
884
885
886
887
  @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
888
889
890
891

  """
  instance_name = args[0]

892
  op = opcodes.OpInstanceConsole(instance_name=instance_name)
893

894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
  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):
910
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
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
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950

  @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)
951
  else:
952
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
953
954

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
955
956


957
def _FormatLogicalID(dev_type, logical_id, roman):
958
959
960
961
962
963
  """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 = [
964
965
966
967
968
      ("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)),
969
970
971
972
973
974
975
976
977
978
979
      ("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


980
def _FormatBlockDevInfo(idx, top_level, dev, static, roman):
Iustin Pop's avatar
Iustin Pop committed
981
982
  """Show block device information.

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

986
987
988
989
  @type idx: int
  @param idx: the index of the current disk
  @type top_level: boolean
  @param top_level: if this a top-level disk?
990
991
992
993
994
  @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
995
996
  @type roman: boolean
  @param roman: whether to try to use roman integers
997
998
  @return: a list of either strings, tuples or lists
      (which should be formatted at a higher indent level)
999

Iustin Pop's avatar
Iustin Pop committed
1000
  """
1001
  def helper(dtype, status):
1002
1003
1004
1005
1006
1007
    """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}
1008
    @return: the string representing the status
1009
1010

    """
Iustin Pop's avatar
Iustin Pop committed
1011
    if not status:
1012
1013
      return "not active"
    txt = ""
1014
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
1015
1016
    if major is None:
      major_string = "N/A"
Iustin Pop's avatar
Iustin Pop committed
1017
    else:
1018
      major_string = str(compat.TryToRoman(major, convert=roman))
1019

1020
1021
1022
    if minor is None:
      minor_string = "N/A"
    else:
1023
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1024
1025
1026
1027
1028
1029

    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:
1030
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1031
        else:
1032
1033
1034
1035
1036
1037
1038
          sync_text += " ETA unknown"
      else:
        sync_text = "in sync"
      if degr:
        degr_text = "*DEGRADED*"
      else:
        degr_text = "ok"
1039
      if ldisk_status == constants.LDS_FAULTY:
1040
        ldisk_text = " *MISSING DISK*"
1041
1042
      elif ldisk_status == constants.LDS_UNKNOWN:
        ldisk_text = " *UNCERTAIN STATE*"
1043
1044
1045
1046
      else:
        ldisk_text = ""
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
    elif dtype == constants.LD_LV:
1047
      if ldisk_status == constants.LDS_FAULTY:
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
        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:
1059
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1060
  else:
1061
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
Iustin Pop's avatar
Iustin Pop committed
1062
1063
1064
1065
1066
  if