gnt_instance.py 54.7 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

23
# pylint: disable=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
48
49
50
51
52
53
54
55
56
57
58
_EXPAND_CLUSTER = "cluster"
_EXPAND_NODES_BOTH = "nodes"
_EXPAND_NODES_PRI = "nodes-pri"
_EXPAND_NODES_SEC = "nodes-sec"
_EXPAND_NODES_BOTH_BY_TAGS = "nodes-by-tags"
_EXPAND_NODES_PRI_BY_TAGS = "nodes-pri-by-tags"
_EXPAND_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
_EXPAND_INSTANCES = "instances"
_EXPAND_INSTANCES_BY_TAGS = "instances-by-tags"

_EXPAND_NODES_TAGS_MODES = frozenset([
  _EXPAND_NODES_BOTH_BY_TAGS,
  _EXPAND_NODES_PRI_BY_TAGS,
  _EXPAND_NODES_SEC_BY_TAGS,
  ])
59

60

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

66

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

70
71
72
  For _EXPAND_CLUSTER, all instances will be returned. For
  _EXPAND_NODES_PRI/SEC, all instances having those nodes as
  primary/secondary will be returned. For _EXPAND_NODES_BOTH, all
73
  instances having those nodes as either primary or secondary will be
74
  returned. For _EXPAND_INSTANCES, the given instances will be
75
76
  returned.

77
78
79
  @param mode: one of L{_EXPAND_CLUSTER}, L{_EXPAND_NODES_BOTH},
      L{_EXPAND_NODES_PRI}, L{_EXPAND_NODES_SEC} or
      L{_EXPAND_INSTANCES}
80
81
82
83
84
85
86
87
88
  @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

89
  """
90
  # pylint: disable=W0142
91

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

101
102
103
  elif (mode in _EXPAND_NODES_TAGS_MODES or
        mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_PRI, _EXPAND_NODES_SEC)):
    if mode in _EXPAND_NODES_TAGS_MODES:
104
105
106
107
108
109
110
111
112
      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 (_EXPAND_NODES_BOTH, _EXPAND_NODES_BOTH_BY_TAGS):
120
      inames = pri_names + sec_names
121
    elif mode in (_EXPAND_NODES_PRI, _EXPAND_NODES_PRI_BY_TAGS):
122
      inames = pri_names
123
    elif mode in (_EXPAND_NODES_SEC, _EXPAND_NODES_SEC_BY_TAGS):
124
125
126
      inames = sec_names
    else:
      raise errors.ProgrammerError("Unhandled shutdown type")
127
  elif mode == _EXPAND_INSTANCES:
128
    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
  elif mode == _EXPAND_INSTANCES_BY_TAGS:
134
135
136
137
138
    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
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:
178
      opts.multi_mode = _EXPAND_INSTANCES
179
180
181
    cl = GetClient()
    inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
    if not inames:
182
      if opts.multi_mode == _EXPAND_CLUSTER:
183
184
        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
    multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
188
    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
  fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
                               "nic.modes", "nic.links", "nic.bridges",
215
                               "snodes", "snodes.group", "snodes.group.uuid"],
216
217
218
                              (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
223
                     format_override=fmtoverride, verbose=opts.verbose,
                     force_filter=opts.force_filter)
224
225
226
227
228
229
230
231
232
233
234
235
236
237


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


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

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

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


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

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

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

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

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

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

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

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

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

333
  jex = JobExecutor(opts=opts)
334

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

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

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

Iustin Pop's avatar
Iustin Pop committed
357
    utils.ForceDictType(specs["backend"], constants.BES_PARAMETER_TYPES)
358
359
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

360
    tmp_nics = []
361
    for field in constants.INIC_PARAMS:
362
363
364
365
366
      if field in specs:
        if not tmp_nics:
          tmp_nics.append({})
        tmp_nics[0][field] = specs[field]

Iustin Pop's avatar
Iustin Pop committed
367
    if specs["nics"] is not None and tmp_nics:
368
      raise errors.OpPrereqError("'nics' list incompatible with using"
369
370
                                 " individual nic fields as well",
                                 errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
371
372
    elif specs["nics"] is not None:
      tmp_nics = specs["nics"]
373
374
375
    elif not tmp_nics:
      tmp_nics = [{}]

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

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

  return 0


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

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

  """
414
415
  # first, compute the desired name list
  if opts.multi_mode is None:
416
    opts.multi_mode = _EXPAND_INSTANCES
417
418
419

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

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

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

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

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

Iustin Pop's avatar
Iustin Pop committed
445
    if selected == "exit":
446
      ToStderr("User aborted reinstall, exiting")
447
448
      return 1

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

543
544
545
  return 0


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

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

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


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

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

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


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

610
611
612
613
614
615
616
617
  if opts.node:
    pnode, snode = SplitNodeOption(opts.node)
    nodes = [pnode]
    if snode is not None:
      nodes.append(snode)
  else:
    nodes = []

618
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
619
620
                                       disks=opts.disks,
                                       nodes=nodes)
Iustin Pop's avatar
Iustin Pop committed
621
622
623
624
  SubmitOrSend(op, opts)
  return 0


Iustin Pop's avatar
Iustin Pop committed
625
def GrowDisk(opts, args):
626
  """Grow an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
627

628
629
630
631
632
633
  @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
634
635
636
637

  """
  instance = args[0]
  disk = args[1]
638
639
  try:
    disk = int(disk)
640
  except (TypeError, ValueError), err:
641
642
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
                               errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
643
  amount = utils.ParseUnit(args[2])
Iustin Pop's avatar
Iustin Pop committed
644
645
646
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
                                  disk=disk, amount=amount,
                                  wait_for_sync=opts.wait_for_sync)
647
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
648
649
650
  return 0


651
def _StartupInstance(name, opts):
652
  """Startup instances.
Iustin Pop's avatar
Iustin Pop committed
653

654
655
  This returns the opcode to start an instance, and its decorator will
  wrap this into a loop starting all desired instances.
656

657
  @param name: the name of the instance to act on
658
  @param opts: the command line options selected by the user
659
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
660
661

  """
662
  op = opcodes.OpInstanceStartup(instance_name=name,
663
                                 force=opts.force,
664
                                 ignore_offline_nodes=opts.ignore_offline,
665
666
                                 no_remember=opts.no_remember,
                                 startup_paused=opts.startup_paused)
667
668
669
670
671
672
  # 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
673

674

675
def _RebootInstance(name, opts):
676
677
  """Reboot instance(s).

678
679
  This returns the opcode to reboot an instance, and its decorator
  will wrap this into a loop rebooting all desired instances.
680

681
  @param name: the name of the instance to act on
682
  @param opts: the command line options selected by the user
683
  @return: the opcode needed for the operation
684
685

  """
686
  return opcodes.OpInstanceReboot(instance_name=name,
687
                                  reboot_type=opts.reboot_type,
688
                                  ignore_secondaries=opts.ignore_secondaries,
689
                                  shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
690

691

692
def _ShutdownInstance(name, opts):
Iustin Pop's avatar
Iustin Pop committed
693
694
  """Shutdown an instance.

695
696
697
698
  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
699
  @param opts: the command line options selected by the user
700
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
701
702

  """
703
  return opcodes.OpInstanceShutdown(instance_name=name,
704
                                    timeout=opts.timeout,
705
706
                                    ignore_offline_nodes=opts.ignore_offline,
                                    no_remember=opts.no_remember)
Iustin Pop's avatar
Iustin Pop committed
707
708
709
710
711


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

712
713
714
715
716
  @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
717
718

  """
719
  new_2ndary = opts.dst_node
720
  iallocator = opts.iallocator
721
  if opts.disks is None:
722
    disks = []
723
  else:
724
725
    try:
      disks = [int(i) for i in opts.disks.split(",")]
726
    except (TypeError, ValueError), err:
727
728
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
                                 errors.ECODE_INVAL)
729
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
730
731
         new_2ndary is not None, iallocator is not None].count(True)
  if cnt != 1:
732
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
733
                               " options must be passed", errors.ECODE_INVAL)
734
  elif opts.on_primary:
735
    mode = constants.REPLACE_DISK_PRI
736
  elif opts.on_secondary:
737
    mode = constants.REPLACE_DISK_SEC
738
739
740
741
  elif opts.auto:
    mode = constants.REPLACE_DISK_AUTO
    if disks:
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
742
                                 " mode", errors.ECODE_INVAL)
743
744
745
  elif new_2ndary is not None or iallocator is not None:
    # replace secondary
    mode = constants.REPLACE_DISK_CHG
746

747
748
749
750
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
                                      remote_node=new_2ndary, mode=mode,
                                      iallocator=iallocator,
                                      early_release=opts.early_release)
751
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
752
753
754
755
756
757
758
759
760
  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.

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

  """
768
  cl = GetClient()
769
770
  instance_name = args[0]
  force = opts.force
771
772
773
774
775
776
  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
777

778
  if not force:
779
780
    _EnsureInstancesExist(cl, [instance_name])

781
782
783
784
785
    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
786

787
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
788
                                  ignore_consistency=opts.ignore_consistency,
789
790
791
                                  shutdown_timeout=opts.shutdown_timeout,
                                  iallocator=iallocator,
                                  target_node=target_node)
792
  SubmitOrSend(op, opts, cl=cl)
793
  return 0
Iustin Pop's avatar
Iustin Pop committed
794
795


796
797
798
799
800
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
801
802
803
804
805
  @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
806
807

  """
808
  cl = GetClient()
809
810
  instance_name = args[0]
  force = opts.force
811
812
813
814
815
816
  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)
817
818

  if not force:
819
820
    _EnsureInstancesExist(cl, [instance_name])

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

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

843
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
844
                                 cleanup=opts.cleanup, iallocator=iallocator,
845
846
                                 target_node=target_node,
                                 allow_failover=opts.allow_failover)
847
  SubmitOpCode(op, cl=cl, opts=opts)
848
849
850
  return 0


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

872
  op = opcodes.OpInstanceMove(instance_name=instance_name,
873
                              target_node=opts.node,
Iustin Pop's avatar
Iustin Pop committed
874
875
                              shutdown_timeout=opts.shutdown_timeout,
                              ignore_consistency=opts.ignore_consistency)
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
893
894
  cl = GetClient()
  try:
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
895
896
    ((console_data, oper_state), ) = \
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
897
898
899
900
901
902
  finally:
    # Ensure client connection is closed while external commands are run
    cl.Close()

  del cl

903
904
905
906
907
908
909
910
911
  if not console_data:
    if oper_state:
      # Instance is running
      raise errors.OpExecError("Console information for instance %s is"
                               " unavailable" % instance_name)
    else:
      raise errors.OpExecError("Instance %s is not running, can't get console" %
                               instance_name)

912
913
914
915
916
917
  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):
918
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936

  @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)
937
938
939
  elif console.kind == constants.CONS_SPICE:
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
                console.host, console.port)
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
  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)
962
  else:
963
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
964
965

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
966
967


968
def _FormatLogicalID(dev_type, logical_id, roman):
969
970
971
972
973
974
  """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 = [
975
976
977
978
979
      ("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)),
980
981
982
983
984
985
986
987
988
989
990
      ("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


991
def _FormatBlockDevInfo(idx, top_level, dev, roman):
Iustin Pop's avatar
Iustin Pop committed
992
993
  """Show block device information.

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