gnt_instance.py 56.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
from ganeti import ht
43
44


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
_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,
  ])
60

61

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

67

68
69
70
_ENV_OVERRIDE = frozenset(["list"])


71
def _ExpandMultiNames(mode, names, client=None):
72
73
  """Expand the given names using the passed mode.

74
75
76
  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
77
  instances having those nodes as either primary or secondary will be
78
  returned. For _EXPAND_INSTANCES, the given instances will be
79
80
  returned.

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

93
  """
94
  # pylint: disable=W0142
95

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

105
106
107
  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:
108
109
110
111
112
113
114
115
116
      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"],
117
                              False)
118

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

  return inames
Iustin Pop's avatar
Iustin Pop committed
147
148


149
150
151
152
153
154
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
155
  @type client: L{ganeti.luxi.Client}
156
157
158
159
160
161
  @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

  """
162
  # TODO: change LUInstanceQuery to that it actually returns None
163
  # instead of raising an exception, or devise a better mechanism
164
  result = client.QueryInstances(names, ["name"], False)
165
166
  for orig_name, row in zip(names, result):
    if row[0] is None:
167
168
      raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
                                 errors.ECODE_NOENT)
169
170


171
172
173
174
175
176
177
178
179
180
181
def GenericManyOps(operation, fn):
  """Generic multi-instance operations.

  The will return a wrapper that processes the options and arguments
  given, and uses the passed function to build the opcode needed for
  the specific operation. Thus all the generic loop/confirmation code
  is abstracted into this function.

  """
  def realfn(opts, args):
    if opts.multi_mode is None:
182
      opts.multi_mode = _EXPAND_INSTANCES
183
184
185
    cl = GetClient()
    inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
    if not inames:
186
      if opts.multi_mode == _EXPAND_CLUSTER:
187
188
        ToStdout("Cluster is empty, no instances to shutdown")
        return 0
189
      raise errors.OpPrereqError("Selection filter does not match"
190
                                 " any instances", errors.ECODE_INVAL)
191
    multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
192
    if not (opts.force_multi or not multi_on
193
            or ConfirmOperation(inames, "instances", operation)):
194
      return 1
195
    jex = JobExecutor(verbose=multi_on, cl=cl, opts=opts)
196
197
198
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
Iustin Pop's avatar
Iustin Pop committed
199
200
201
    results = jex.WaitOrShow(not opts.submit_only)
    rcode = compat.all(row[0] for row in results)
    return int(not rcode)
202
203
204
  return realfn


Iustin Pop's avatar
Iustin Pop committed
205
def ListInstances(opts, args):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
206
  """List instances and their properties.
Iustin Pop's avatar
Iustin Pop committed
207

208
209
210
211
212
213
  @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
214
  """
215
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
Iustin Pop's avatar
Iustin Pop committed
216

217
218
  fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
                               "nic.modes", "nic.links", "nic.bridges",
219
                               "snodes", "snodes.group", "snodes.group.uuid"],
220
221
222
                              (lambda value: ",".join(str(item)
                                                      for item in value),
                               False))
Iustin Pop's avatar
Iustin Pop committed
223

224
225
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
                     opts.separator, not opts.no_headers,
226
227
                     format_override=fmtoverride, verbose=opts.verbose,
                     force_filter=opts.force_filter)
228
229
230
231
232
233
234
235
236
237
238
239
240
241


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
242
243
244
245
246


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

Iustin Pop's avatar
Iustin Pop committed
247
  This is just a wrapper over GenericInstanceCreate.
Iustin Pop's avatar
Iustin Pop committed
248
249

  """
Iustin Pop's avatar
Iustin Pop committed
250
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
Iustin Pop's avatar
Iustin Pop committed
251
252


253
def BatchCreate(opts, args):
254
255
256
257
258
259
  """Create instances using a definition file.

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

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

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

  def _PopulateWithDefaults(spec):
    """Returns a new hash combined with default values."""
298
299
300
    mydict = _DEFAULT_SPECS.copy()
    mydict.update(spec)
    return mydict
301
302
303
304

  def _Validate(spec):
    """Validate the instance specs."""
    # Validate fields required under any circumstances
Iustin Pop's avatar
Iustin Pop committed
305
    for required_field in ("os", "template"):
306
307
      if required_field not in spec:
        raise errors.OpPrereqError('Required field "%s" is missing.' %
308
                                   required_field, errors.ECODE_INVAL)
309
    # Validate special fields
Iustin Pop's avatar
Iustin Pop committed
310
311
312
313
314
    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.",
315
                                   errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
316
317
318
    elif spec["iallocator"] is None:
      raise errors.OpPrereqError("You have to provide at least a primary_node"
                                 " or an iallocator.",
319
                                 errors.ECODE_INVAL)
320

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

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

Guido Trotter's avatar
Guido Trotter committed
333
334
335
336
  if not isinstance(instance_data, dict):
    ToStderr("The instance definition file is not in dict format.")
    return 1

337
  jex = JobExecutor(opts=opts)
338

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

Iustin Pop's avatar
Iustin Pop committed
348
349
    hypervisor = specs["hypervisor"]
    hvparams = specs["hvparams"]
350

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

361
    utils.ForceDictType(specs["backend"], constants.BES_PARAMETER_COMPAT)
362
363
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

364
    tmp_nics = []
365
    for field in constants.INIC_PARAMS:
366
367
368
369
370
      if field in specs:
        if not tmp_nics:
          tmp_nics.append({})
        tmp_nics[0][field] = specs[field]

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

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

400
401
402
    jex.QueueJob(name, op)
  # we never want to wait, just show the submitted job IDs
  jex.WaitOrShow(False)
403
404
405
406

  return 0


407
408
409
def ReinstallInstance(opts, args):
  """Reinstall an instance.

410
411
412
413
414
415
  @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
416
417

  """
418
419
  # first, compute the desired name list
  if opts.multi_mode is None:
420
    opts.multi_mode = _EXPAND_INSTANCES
421
422
423

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

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

    if not result:
433
      ToStdout("Can't get the OS list")
434
435
      return 1

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

Iustin Pop's avatar
Iustin Pop committed
445
    choices.append(("x", "exit", "Exit gnt-instance reinstall"))
446
    selected = AskUser("Enter OS template number (or x to abort):",
447
448
                       choices)

Iustin Pop's avatar
Iustin Pop committed
449
    if selected == "exit":
450
      ToStderr("User aborted reinstall, exiting")
451
452
      return 1

453
    os_name = selected
454
    os_msg = "change the OS to '%s'" % selected
455
  else:
456
    os_name = opts.os
457
458
459
460
    if opts.os is not None:
      os_msg = "change the OS to '%s'" % os_name
    else:
      os_msg = "keep the same OS"
461

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

479
  jex = JobExecutor(verbose=multi_on, opts=opts)
480
  for instance_name in inames:
481
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
482
                                     os_type=os_name,
483
484
                                     force_variant=opts.force_variant,
                                     osparams=opts.osparams)
485
    jex.QueueJob(instance_name, op)
486

487
  jex.WaitOrShow(not opts.submit_only)
488
489
490
  return 0


Iustin Pop's avatar
Iustin Pop committed
491
492
493
def RemoveInstance(opts, args):
  """Remove an instance.

494
495
496
497
498
499
  @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
500
501
502
503

  """
  instance_name = args[0]
  force = opts.force
504
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
505
506

  if not force:
507
508
    _EnsureInstancesExist(cl, [instance_name])

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

515
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
516
                                ignore_failures=opts.ignore_failures,
517
                                shutdown_timeout=opts.shutdown_timeout)
518
  SubmitOrSend(op, opts, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
519
520
521
  return 0


522
def RenameInstance(opts, args):
Guido Trotter's avatar
Guido Trotter committed
523
  """Rename an instance.
524

525
526
527
528
529
530
  @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
531
532

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

538
  op = opcodes.OpInstanceRename(instance_name=args[0],
539
                                new_name=args[1],
540
541
                                ip_check=opts.ip_check,
                                name_check=opts.name_check)
542
543
  result = SubmitOrSend(op, opts)

544
545
  if result:
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
546

547
548
549
  return 0


Iustin Pop's avatar
Iustin Pop committed
550
551
552
553
def ActivateDisks(opts, args):
  """Activate an instance's disks.

  This serves two purposes:
554
555
    - 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
556
557
    - it repairs inactive secondary drbds

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


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

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

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


Iustin Pop's avatar
Iustin Pop committed
594
595
596
597
598
599
600
601
602
603
604
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]
605
606
607

  disks = []

Iustin Pop's avatar
Iustin Pop committed
608
  if opts.disks:
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
    for didx, ddict in opts.disks:
      didx = int(didx)

      if not ht.TDict(ddict):
        msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
        raise errors.OpPrereqError(msg)

      if constants.IDISK_SIZE in ddict:
        try:
          ddict[constants.IDISK_SIZE] = \
            utils.ParseUnit(ddict[constants.IDISK_SIZE])
        except ValueError, err:
          raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
                                     (didx, err))

      disks.append((didx, ddict))

    # TODO: Verify modifyable parameters (already done in
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
Iustin Pop's avatar
Iustin Pop committed
628

629
630
631
632
633
634
635
636
  if opts.node:
    pnode, snode = SplitNodeOption(opts.node)
    nodes = [pnode]
    if snode is not None:
      nodes.append(snode)
  else:
    nodes = []

637
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
638
                                       disks=disks, nodes=nodes)
Iustin Pop's avatar
Iustin Pop committed
639
  SubmitOrSend(op, opts)
640

Iustin Pop's avatar
Iustin Pop committed
641
642
643
  return 0


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

647
648
  @param opts: the command line options selected by the user
  @type args: list
Guido Trotter's avatar
Guido Trotter committed
649
650
  @param args: should contain three elements, the target instance name,
      the target disk id, and the target growth
651
652
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
653
654
655
656

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


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

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

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

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

693

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

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

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

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

710

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

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

  """
722
  return opcodes.OpInstanceShutdown(instance_name=name,
723
                                    timeout=opts.timeout,
724
725
                                    ignore_offline_nodes=opts.ignore_offline,
                                    no_remember=opts.no_remember)
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
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
                                      remote_node=new_2ndary, mode=mode,
                                      iallocator=iallocator,
769
770
                                      early_release=opts.early_release,
                                      ignore_ipolicy=opts.ignore_ipolicy)
771
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
772
773
774
775
776
777
778
779
780
  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.

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

  """
788
  cl = GetClient()
789
790
  instance_name = args[0]
  force = opts.force
791
792
793
794
795
796
  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
797

798
  if not force:
799
800
    _EnsureInstancesExist(cl, [instance_name])

801
802
803
804
805
    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
806

807
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
808
                                  ignore_consistency=opts.ignore_consistency,
809
810
                                  shutdown_timeout=opts.shutdown_timeout,
                                  iallocator=iallocator,
811
812
                                  target_node=target_node,
                                  ignore_ipolicy=opts.ignore_ipolicy)
813
  SubmitOrSend(op, opts, cl=cl)
814
  return 0
Iustin Pop's avatar
Iustin Pop committed
815
816


817
818
819
820
821
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
822
823
824
825
826
  @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
827
828

  """
829
  cl = GetClient()
830
831
  instance_name = args[0]
  force = opts.force
832
833
834
835
836
837
  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)
838
839

  if not force:
840
841
    _EnsureInstancesExist(cl, [instance_name])

842
843
844
845
846
847
848
    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
849
850
    usertext += (" might impact the instance if anything goes wrong"
                 " (e.g. due to bugs in the hypervisor). Continue?")
851
852
853
    if not AskUser(usertext):
      return 1

854
  # this should be removed once --non-live is deprecated
855
  if not opts.live and opts.migration_mode is not None:
856
    raise errors.OpPrereqError("Only one of the --non-live and "
857
                               "--migration-mode options can be passed",
858
859
                               errors.ECODE_INVAL)
  if not opts.live: # --non-live passed
860
    mode = constants.HT_MIGRATION_NONLIVE
861
  else:
862
    mode = opts.migration_mode
863

864
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
865
                                 cleanup=opts.cleanup, iallocator=iallocator,
866
                                 target_node=target_node,
867
                                 allow_failover=opts.allow_failover,
868
                                 allow_runtime_changes=opts.allow_runtime_chgs,
869
                                 ignore_ipolicy=opts.ignore_ipolicy)
870
  SubmitOpCode(op, cl=cl, opts=opts)
871
872
873
  return 0


Iustin Pop's avatar
Iustin Pop committed
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
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

895
  op = opcodes.OpInstanceMove(instance_name=instance_name,
896
                              target_node=opts.node,
Iustin Pop's avatar
Iustin Pop committed
897
                              shutdown_timeout=opts.shutdown_timeout,
898
899
                              ignore_consistency=opts.ignore_consistency,
                              ignore_ipolicy=opts.ignore_ipolicy)
Iustin Pop's avatar
Iustin Pop committed
900
901
902
903
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
904
905
906
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

907
908
909
910
911
  @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
912
913
914
915

  """
  instance_name = args[0]

916
917
918
  cl = GetClient()
  try:
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
919
920
    ((console_data, oper_state), ) = \
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
921
922
923
924
925
926
  finally:
    # Ensure client connection is closed while external commands are run
    cl.Close()

  del cl

927
928
929
930
931
932
933
934
935
  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)

936
937
938
939
940
941
  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):
942
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960

  @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)
961
962
963
  elif console.kind == constants.CONS_SPICE:
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
                console.host, console.port)
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
  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)
986
  else:
987
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
988
989

  return constants.EXIT_SUCCESS
Iustin Pop's avatar
Iustin Pop committed
990
991


992
def _FormatLogicalID(dev_type, logical_id, roman):
993
994
995
996
997
998
  """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 = [
999
1000
1001
1002
1003
      ("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)),
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
      ("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


1015
def _FormatBlockDevInfo(idx, top_level, dev, roman):
Iustin Pop's avatar
Iustin Pop committed
1016
1017
  """Show block device information.

1018
  This is only used by L{ShowInstanceConfig}, but it's too big to be
Iustin Pop's avatar