gnt_instance.py 52.5 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
def _ConfirmOperation(inames, text, extra=""):
146
147
148
149
150
  """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.

151
152
153
154
155
156
157
158
  @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.
159
160
161

  """
  count = len(inames)
162
163
  msg = ("The %s will operate on %d instances.\n%s"
         "Do you want to continue?" % (text, count, extra))
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
  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)
179
    choice = AskUser(msg + affected, choices)
180
181
182
  return choice


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

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


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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:
220
221
222
      if opts.multi_mode == _SHUTDOWN_CLUSTER:
        ToStdout("Cluster is empty, no instances to shutdown")
        return 0
223
      raise errors.OpPrereqError("Selection filter does not match"
224
                                 " any instances", errors.ECODE_INVAL)
225
226
227
228
    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
229
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
230
231
232
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
Iustin Pop's avatar
Iustin Pop committed
233
234
235
    results = jex.WaitOrShow(not opts.submit_only)
    rcode = compat.all(row[0] for row in results)
    return int(not rcode)
236
237
238
  return realfn


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

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

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

258
259
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
                     opts.separator, not opts.no_headers,
260
                     format_override=fmtoverride, verbose=opts.verbose)
261
262
263
264
265
266
267
268
269
270
271
272
273
274


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
275
276
277
278
279


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

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

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


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

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

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

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

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

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

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

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

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

370
  jex = JobExecutor(opts=opts)
371

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

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

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

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

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

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

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

  return 0


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

574
575
576
  return 0


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

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

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


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

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

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


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

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


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

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

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


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

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

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

  """
684
  op = opcodes.OpInstanceStartup(instance_name=name,
685
686
                                 force=opts.force,
                                 ignore_offline_nodes=opts.ignore_offline)
687
688
689
690
691
692
  # 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
693

694

695
def _RebootInstance(name, opts):
696
697
  """Reboot instance(s).

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

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

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

711

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

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

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


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

731
732
733
734
735
  @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
736
737

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

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

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
Iustin Pop's avatar
Iustin Pop committed
785
786

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

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

794
795
796
797
798
    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
799

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


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

  The migrate is done without shutdown.

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

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

  if not force:
824
825
    _EnsureInstancesExist(cl, [instance_name])

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

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

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


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

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


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

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

  """
  instance_name = args[0]

894
  op = opcodes.OpInstanceConsole(instance_name=instance_name)
895

896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
  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):
912
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
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
951
952

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

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
957
958


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


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

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

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

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

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

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

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