gnt-instance 47 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
#

# Copyright (C) 2006, 2007 Google Inc.
#
# 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

Iustin Pop's avatar
Iustin Pop committed
28
29
import sys
import os
30
import itertools
31
import simplejson
32
import time
Iustin Pop's avatar
Iustin Pop committed
33
34
35
36
37
38
from cStringIO import StringIO

from ganeti.cli import *
from ganeti import opcodes
from ganeti import constants
from ganeti import utils
39
40
41
42
43
44
45
46
47
from ganeti import errors


_SHUTDOWN_CLUSTER = "cluster"
_SHUTDOWN_NODES_BOTH = "nodes"
_SHUTDOWN_NODES_PRI = "nodes-pri"
_SHUTDOWN_NODES_SEC = "nodes-sec"
_SHUTDOWN_INSTANCES = "instances"

48

49
50
_VALUE_TRUE = "true"

51
#: default list of options for L{ListInstances}
52
_LIST_DEF_FIELDS = [
53
  "name", "hypervisor", "os", "pnode", "status", "oper_ram",
54
55
  ]

56

57
def _ExpandMultiNames(mode, names, client=None):
58
59
60
61
  """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
62
  primary/secondary will be returned. For _SHUTDOWN_NODES_BOTH, all
63
64
65
66
  instances having those nodes as either primary or secondary will be
  returned. For _SHUTDOWN_INSTANCES, the given instances will be
  returned.

67
68
69
70
71
72
73
74
75
76
77
78
  @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

79
  """
Iustin Pop's avatar
Iustin Pop committed
80
  # pylint: disable-msg=W0142
81
82
  if client is None:
    client = GetClient()
83
84
  if mode == _SHUTDOWN_CLUSTER:
    if names:
85
86
      raise errors.OpPrereqError("Cluster filter mode takes no arguments",
                                 errors.ECODE_INVAL)
87
    idata = client.QueryInstances([], ["name"], False)
88
89
90
91
92
93
    inames = [row[0] for row in idata]

  elif mode in (_SHUTDOWN_NODES_BOTH,
                _SHUTDOWN_NODES_PRI,
                _SHUTDOWN_NODES_SEC):
    if not names:
94
      raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
95
    ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
96
                              False)
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
    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))
    if mode == _SHUTDOWN_NODES_BOTH:
      inames = pri_names + sec_names
    elif mode == _SHUTDOWN_NODES_PRI:
      inames = pri_names
    elif mode == _SHUTDOWN_NODES_SEC:
      inames = sec_names
    else:
      raise errors.ProgrammerError("Unhandled shutdown type")

  elif mode == _SHUTDOWN_INSTANCES:
    if not names:
112
113
      raise errors.OpPrereqError("No instance names passed",
                                 errors.ECODE_INVAL)
114
    idata = client.QueryInstances(names, ["name"], False)
115
116
117
    inames = [row[0] for row in idata]

  else:
118
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
119
120

  return inames
Iustin Pop's avatar
Iustin Pop committed
121
122


123
def _ConfirmOperation(inames, text, extra=""):
124
125
126
127
128
  """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.

129
130
131
132
133
134
135
136
  @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.
137
138
139

  """
  count = len(inames)
140
141
  msg = ("The %s will operate on %d instances.\n%s"
         "Do you want to continue?" % (text, count, extra))
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
  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)
157
    choice = AskUser(msg + affected, choices)
158
159
160
  return choice


161
162
163
164
165
166
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
167
  @type client: L{ganeti.luxi.Client}
168
169
170
171
172
173
174
175
  @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

  """
  # TODO: change LUQueryInstances to that it actually returns None
  # instead of raising an exception, or devise a better mechanism
176
  result = client.QueryInstances(names, ["name"], False)
177
178
  for orig_name, row in zip(names, result):
    if row[0] is None:
179
180
      raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
                                 errors.ECODE_NOENT)
181
182


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def GenericManyOps(operation, fn):
  """Generic multi-instance operations.

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

  """
  def realfn(opts, args):
    if opts.multi_mode is None:
      opts.multi_mode = _SHUTDOWN_INSTANCES
    cl = GetClient()
    inames = _ExpandMultiNames(opts.multi_mode, args, client=cl)
    if not inames:
      raise errors.OpPrereqError("Selection filter does not match"
199
                                 " any instances", errors.ECODE_INVAL)
200
201
202
203
204
205
206
207
208
209
210
211
212
    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
    jex = JobExecutor(verbose=multi_on, cl=cl)
    for name in inames:
      op = fn(name, opts)
      jex.QueueJob(name, op)
    jex.WaitOrShow(not opts.submit_only)
    return 0
  return realfn


Iustin Pop's avatar
Iustin Pop committed
213
def ListInstances(opts, args):
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
214
  """List instances and their properties.
Iustin Pop's avatar
Iustin Pop committed
215

216
217
218
219
220
221
  @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
222
223
  """
  if opts.output is None:
224
225
226
    selected_fields = _LIST_DEF_FIELDS
  elif opts.output.startswith("+"):
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
Iustin Pop's avatar
Iustin Pop committed
227
228
229
  else:
    selected_fields = opts.output.split(",")

230
  output = GetClient().QueryInstances(args, selected_fields, opts.do_locking)
Iustin Pop's avatar
Iustin Pop committed
231
232

  if not opts.no_headers:
233
234
235
    headers = {
      "name": "Instance", "os": "OS", "pnode": "Primary_node",
      "snodes": "Secondary_Nodes", "admin_state": "Autostart",
Iustin Pop's avatar
Iustin Pop committed
236
      "oper_state": "Running",
237
      "oper_ram": "Memory", "disk_template": "Disk_template",
238
      "ip": "IP_address", "mac": "MAC_address",
239
      "nic_mode": "NIC_Mode", "nic_link": "NIC_Link",
Iustin Pop's avatar
Iustin Pop committed
240
      "bridge": "Bridge",
241
      "sda_size": "Disk/0", "sdb_size": "Disk/1",
242
      "disk_usage": "DiskUsage",
243
      "status": "Status", "tags": "Tags",
244
      "network_port": "Network_port",
245
246
      "hv/kernel_path": "Kernel_path",
      "hv/initrd_path": "Initrd_path",
Iustin Pop's avatar
Iustin Pop committed
247
248
249
250
251
252
      "hv/boot_order": "Boot_order",
      "hv/acpi": "ACPI",
      "hv/pae": "PAE",
      "hv/cdrom_image_path": "CDROM_image_path",
      "hv/nic_type": "NIC_type",
      "hv/disk_type": "Disk_type",
253
      "hv/vnc_bind_address": "VNC_bind_address",
254
      "serial_no": "SerialNo", "hypervisor": "Hypervisor",
255
      "hvparams": "Hypervisor_parameters",
Iustin Pop's avatar
Iustin Pop committed
256
257
      "be/memory": "Configured_memory",
      "be/vcpus": "VCPUs",
258
      "vcpus": "VCPUs",
259
      "be/auto_balance": "Auto_balance",
Iustin Pop's avatar
Iustin Pop committed
260
261
      "disk.count": "Disks", "disk.sizes": "Disk_sizes",
      "nic.count": "NICs", "nic.ips": "NIC_IPs",
262
      "nic.modes": "NIC_modes", "nic.links": "NIC_links",
Iustin Pop's avatar
Iustin Pop committed
263
      "nic.bridges": "NIC_bridges", "nic.macs": "NIC_MACs",
264
      "ctime": "CTime", "mtime": "MTime", "uuid": "UUID",
265
      }
266
267
268
  else:
    headers = None

269
  unitfields = ["be/memory", "oper_ram", "sd(a|b)_size", "disk\.size/.*"]
270
  numfields = ["be/memory", "oper_ram", "sd(a|b)_size", "be/vcpus",
Iustin Pop's avatar
Iustin Pop committed
271
               "serial_no", "(disk|nic)\.count", "disk\.size/.*"]
272

273
274
  list_type_fields = ("tags", "disk.sizes", "nic.macs", "nic.ips",
                      "nic.modes", "nic.links", "nic.bridges")
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
  # change raw values to nicer strings
  for row in output:
    for idx, field in enumerate(selected_fields):
      val = row[idx]
      if field == "snodes":
        val = ",".join(val) or "-"
      elif field == "admin_state":
        if val:
          val = "yes"
        else:
          val = "no"
      elif field == "oper_state":
        if val is None:
          val = "(node down)"
        elif val: # True
          val = "running"
        else:
          val = "stopped"
      elif field == "oper_ram":
        if val is None:
          val = "(node down)"
      elif field == "sda_size" or field == "sdb_size":
        if val is None:
          val = "N/A"
299
300
      elif field == "ctime" or field == "mtime":
        val = utils.FormatTime(val)
301
      elif field in list_type_fields:
Iustin Pop's avatar
Iustin Pop committed
302
        val = ",".join(str(item) for item in val)
303
304
      elif val is None:
        val = "-"
305
306
      row[idx] = str(val)

307
308
  data = GenerateTable(separator=opts.separator, headers=headers,
                       fields=selected_fields, unitfields=unitfields,
309
                       numfields=numfields, data=output, units=opts.units)
310
311

  for line in data:
312
    ToStdout(line)
Iustin Pop's avatar
Iustin Pop committed
313
314
315
316
317
318
319

  return 0


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

Iustin Pop's avatar
Iustin Pop committed
320
  This is just a wrapper over GenericInstanceCreate.
Iustin Pop's avatar
Iustin Pop committed
321
322

  """
Iustin Pop's avatar
Iustin Pop committed
323
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
Iustin Pop's avatar
Iustin Pop committed
324
325


326
def BatchCreate(opts, args):
327
328
329
330
331
332
  """Create instances using a definition file.

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

    {"instance-name":{
333
      "disk_size": [20480],
334
335
336
337
      "template": "drbd",
      "backend": {
        "memory": 512,
        "vcpus": 1 },
338
      "os": "debootstrap",
339
340
341
342
343
344
345
346
347
348
349
350
351
      "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
352
353

  """
354
  _DEFAULT_SPECS = {"disk_size": [20 * 1024],
355
356
357
358
                    "backend": {},
                    "iallocator": None,
                    "primary_node": None,
                    "secondary_node": None,
359
                    "nics": None,
360
361
                    "start": True,
                    "ip_check": True,
362
                    "name_check": True,
363
                    "hypervisor": None,
Iustin Pop's avatar
Iustin Pop committed
364
                    "hvparams": {},
365
366
367
368
369
                    "file_storage_dir": None,
                    "file_driver": 'loop'}

  def _PopulateWithDefaults(spec):
    """Returns a new hash combined with default values."""
370
371
372
    mydict = _DEFAULT_SPECS.copy()
    mydict.update(spec)
    return mydict
373
374
375
376
377
378
379

  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.' %
380
                                   required_field, errors.ECODE_INVAL)
381
382
383
384
385
    # 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'
386
387
                                   ' there was no secondary provided.',
                                   errors.ECODE_INVAL)
388
389
    elif spec['iallocator'] is None:
      raise errors.OpPrereqError('You have to provide at least a primary_node'
390
391
                                 ' or an iallocator.',
                                 errors.ECODE_INVAL)
392

Iustin Pop's avatar
Iustin Pop committed
393
394
    if (spec['hvparams'] and
        not isinstance(spec['hvparams'], dict)):
395
396
      raise errors.OpPrereqError('Hypervisor parameters must be a dict.',
                                 errors.ECODE_INVAL)
397
398
399

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

405
406
  jex = JobExecutor()

407
408
409
  # Iterate over the instances and do:
  #  * Populate the specs with default value
  #  * Validate the instance specs
410
411
412
  i_names = utils.NiceSort(instance_data.keys())
  for name in i_names:
    specs = instance_data[name]
413
414
415
    specs = _PopulateWithDefaults(specs)
    _Validate(specs)

Iustin Pop's avatar
Iustin Pop committed
416
417
    hypervisor = specs['hypervisor']
    hvparams = specs['hvparams']
418

419
420
421
422
423
424
425
    disks = []
    for elem in specs['disk_size']:
      try:
        size = utils.ParseUnit(elem)
      except ValueError, err:
        raise errors.OpPrereqError("Invalid disk size '%s' for"
                                   " instance %s: %s" %
426
                                   (elem, name, err), errors.ECODE_INVAL)
427
428
      disks.append({"size": size})

429
430
431
    utils.ForceDictType(specs['backend'], constants.BES_PARAMETER_TYPES)
    utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)

432
433
434
435
436
437
438
439
440
    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"
441
442
                                 " individual nic fields as well",
                                 errors.ECODE_INVAL)
443
444
445
446
447
    elif specs['nics'] is not None:
      tmp_nics = specs['nics']
    elif not tmp_nics:
      tmp_nics = [{}]

448
    op = opcodes.OpCreateInstance(instance_name=name,
449
                                  disks=disks,
450
451
452
                                  disk_template=specs['template'],
                                  mode=constants.INSTANCE_CREATE,
                                  os_type=specs['os'],
453
                                  force_variant=opts.force_variant,
454
455
                                  pnode=specs['primary_node'],
                                  snode=specs['secondary_node'],
456
                                  nics=tmp_nics,
457
458
                                  start=specs['start'],
                                  ip_check=specs['ip_check'],
459
                                  name_check=specs['name_check'],
460
461
462
463
464
465
466
467
                                  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'])

468
469
470
    jex.QueueJob(name, op)
  # we never want to wait, just show the submitted job IDs
  jex.WaitOrShow(False)
471
472
473
474

  return 0


475
476
477
def ReinstallInstance(opts, args):
  """Reinstall an instance.

478
479
480
481
482
483
  @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
484
485

  """
486
487
488
489
490
491
  # 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:
492
493
    raise errors.OpPrereqError("Selection filter does not match any instances",
                               errors.ECODE_INVAL)
494

495
  # second, if requested, ask for an OS
496
  if opts.select_os is True:
497
498
    op = opcodes.OpDiagnoseOS(output_fields=["name", "valid", "variants"],
                              names=[])
499
500
501
    result = SubmitOpCode(op)

    if not result:
502
      ToStdout("Can't get the OS list")
503
504
      return 1

505
    ToStdout("Available OS templates:")
506
507
    number = 0
    choices = []
508
509
510
511
512
513
    for (name, valid, variants) in result:
      if valid:
        for entry in CalculateOSNames(name, variants):
          ToStdout("%3s: %s", number, entry)
          choices.append(("%s" % number, entry, entry))
          number += 1
514
515

    choices.append(('x', 'exit', 'Exit gnt-instance reinstall'))
516
    selected = AskUser("Enter OS template number (or x to abort):",
517
518
519
                       choices)

    if selected == 'exit':
520
      ToStderr("User aborted reinstall, exiting")
521
522
      return 1

523
    os_name = selected
524
  else:
525
    os_name = opts.os
526

527
528
529
530
531
532
533
  # third, get confirmation: multi-reinstall requires --force-multi
  # *and* --force, single-reinstall just --force
  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"
    if not ((opts.force_multi and opts.force) or
            _ConfirmOperation(inames, "reinstall", extra=warn_msg)):
534
      return 1
535
536
537
  else:
    if not opts.force:
      usertext = ("This will reinstall the instance %s and remove"
Iustin Pop's avatar
Iustin Pop committed
538
                  " all data. Continue?") % inames[0]
539
540
541
542
543
544
      if not AskUser(usertext):
        return 1

  jex = JobExecutor(verbose=multi_on)
  for instance_name in inames:
    op = opcodes.OpReinstallInstance(instance_name=instance_name,
545
546
                                     os_type=os_name,
                                     force_variant=opts.force_variant)
547
    jex.QueueJob(instance_name, op)
548

549
  jex.WaitOrShow(not opts.submit_only)
550
551
552
  return 0


Iustin Pop's avatar
Iustin Pop committed
553
554
555
def RemoveInstance(opts, args):
  """Remove an instance.

556
557
558
559
560
561
  @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
562
563
564
565

  """
  instance_name = args[0]
  force = opts.force
566
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
567
568

  if not force:
569
570
    _EnsureInstancesExist(cl, [instance_name])

Iustin Pop's avatar
Iustin Pop committed
571
572
573
    usertext = ("This will remove the volumes of the instance %s"
                " (including mirrors), thus removing all the data"
                " of the instance. Continue?") % instance_name
574
    if not AskUser(usertext):
Iustin Pop's avatar
Iustin Pop committed
575
576
      return 1

Iustin Pop's avatar
Iustin Pop committed
577
  op = opcodes.OpRemoveInstance(instance_name=instance_name,
578
                                ignore_failures=opts.ignore_failures,
579
                                shutdown_timeout=opts.shutdown_timeout)
580
  SubmitOrSend(op, opts, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
581
582
583
  return 0


584
def RenameInstance(opts, args):
Guido Trotter's avatar
Guido Trotter committed
585
  """Rename an instance.
586

587
588
589
590
591
592
  @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
593
594
595
596
597

  """
  op = opcodes.OpRenameInstance(instance_name=args[0],
                                new_name=args[1],
                                ignore_ip=opts.ignore_ip)
598
  SubmitOrSend(op, opts)
599
600
601
  return 0


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

  This serves two purposes:
606
607
    - 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
608
609
    - it repairs inactive secondary drbds

610
611
612
613
614
615
  @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
616
617
  """
  instance_name = args[0]
618
619
  op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
                                       ignore_size=opts.ignore_size)
620
  disks_info = SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
621
  for host, iname, nname in disks_info:
622
    ToStdout("%s:%s:%s", host, iname, nname)
Iustin Pop's avatar
Iustin Pop committed
623
624
625
626
  return 0


def DeactivateDisks(opts, args):
Iustin Pop's avatar
Iustin Pop committed
627
  """Deactivate an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
628
629
630
631

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

632
633
634
635
636
637
  @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
638
639
640
  """
  instance_name = args[0]
  op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
641
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
642
643
644
  return 0


Iustin Pop's avatar
Iustin Pop committed
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
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 = []

  op = opcodes.OpRecreateInstanceDisks(instance_name=instance_name,
                                       disks=opts.disks)
  SubmitOrSend(op, opts)
  return 0


Iustin Pop's avatar
Iustin Pop committed
671
def GrowDisk(opts, args):
672
  """Grow an instance's disks.
Iustin Pop's avatar
Iustin Pop committed
673

674
675
676
677
678
679
  @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
680
681
682
683

  """
  instance = args[0]
  disk = args[1]
684
685
686
  try:
    disk = int(disk)
  except ValueError, err:
687
688
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
                               errors.ECODE_INVAL)
Iustin Pop's avatar
Iustin Pop committed
689
  amount = utils.ParseUnit(args[2])
690
691
  op = opcodes.OpGrowDisk(instance_name=instance, disk=disk, amount=amount,
                          wait_for_sync=opts.wait_for_sync)
692
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
693
694
695
  return 0


696
def _StartupInstance(name, opts):
697
  """Startup instances.
Iustin Pop's avatar
Iustin Pop committed
698

699
700
  This returns the opcode to start an instance, and its decorator will
  wrap this into a loop starting all desired instances.
701

702
  @param name: the name of the instance to act on
703
  @param opts: the command line options selected by the user
704
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
705
706

  """
707
708
709
710
711
712
713
714
  op = opcodes.OpStartupInstance(instance_name=name,
                                 force=opts.force)
  # 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
715

716

717
def _RebootInstance(name, opts):
718
719
  """Reboot instance(s).

720
721
  This returns the opcode to reboot an instance, and its decorator
  will wrap this into a loop rebooting all desired instances.
722

723
  @param name: the name of the instance to act on
724
  @param opts: the command line options selected by the user
725
  @return: the opcode needed for the operation
726
727

  """
728
  return opcodes.OpRebootInstance(instance_name=name,
729
                                  reboot_type=opts.reboot_type,
730
                                  ignore_secondaries=opts.ignore_secondaries,
731
                                  shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
732

733

734
def _ShutdownInstance(name, opts):
Iustin Pop's avatar
Iustin Pop committed
735
736
  """Shutdown an instance.

737
738
739
740
  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
741
  @param opts: the command line options selected by the user
742
  @return: the opcode needed for the operation
Iustin Pop's avatar
Iustin Pop committed
743
744

  """
745
746
  return opcodes.OpShutdownInstance(instance_name=name,
                                    timeout=opts.timeout)
Iustin Pop's avatar
Iustin Pop committed
747
748
749
750
751


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

752
753
754
755
756
  @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
757
758
759

  """
  instance_name = args[0]
760
  new_2ndary = opts.dst_node
761
  iallocator = opts.iallocator
762
  if opts.disks is None:
763
    disks = []
764
  else:
765
766
767
    try:
      disks = [int(i) for i in opts.disks.split(",")]
    except ValueError, err:
768
769
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
                                 errors.ECODE_INVAL)
770
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
771
772
         new_2ndary is not None, iallocator is not None].count(True)
  if cnt != 1:
773
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -i"
774
                               " options must be passed", errors.ECODE_INVAL)
775
  elif opts.on_primary:
776
    mode = constants.REPLACE_DISK_PRI
777
  elif opts.on_secondary:
778
    mode = constants.REPLACE_DISK_SEC
779
780
781
782
  elif opts.auto:
    mode = constants.REPLACE_DISK_AUTO
    if disks:
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
783
                                 " mode", errors.ECODE_INVAL)
784
785
786
  elif new_2ndary is not None or iallocator is not None:
    # replace secondary
    mode = constants.REPLACE_DISK_CHG
787
788

  op = opcodes.OpReplaceDisks(instance_name=args[0], disks=disks,
789
790
                              remote_node=new_2ndary, mode=mode,
                              iallocator=iallocator)
791
  SubmitOrSend(op, opts)
Iustin Pop's avatar
Iustin Pop committed
792
793
794
795
796
797
798
799
800
  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.

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
Iustin Pop's avatar
Iustin Pop committed
806
807

  """
808
  cl = GetClient()
809
810
  instance_name = args[0]
  force = opts.force
Iustin Pop's avatar
Iustin Pop committed
811

812
  if not force:
813
814
    _EnsureInstancesExist(cl, [instance_name])

815
816
817
818
819
    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
820

821
  op = opcodes.OpFailoverInstance(instance_name=instance_name,
822
                                  ignore_consistency=opts.ignore_consistency,
823
                                  shutdown_timeout=opts.shutdown_timeout)
824
  SubmitOrSend(op, opts, cl=cl)
825
  return 0
Iustin Pop's avatar
Iustin Pop committed
826
827


828
829
830
831
832
def MigrateInstance(opts, args):
  """Migrate an instance.

  The migrate is done without shutdown.

Iustin Pop's avatar
Iustin Pop committed
833
834
835
836
837
  @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
838
839

  """
840
  cl = GetClient()
841
842
843
844
  instance_name = args[0]
  force = opts.force

  if not force:
845
846
    _EnsureInstancesExist(cl, [instance_name])

847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
    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,))
    usertext += (" is **experimental** in this version."
                " This might impact the instance if anything goes wrong."
                " Continue?")
    if not AskUser(usertext):
      return 1

  op = opcodes.OpMigrateInstance(instance_name=instance_name, live=opts.live,
                                 cleanup=opts.cleanup)
862
  SubmitOpCode(op, cl=cl)
863
864
865
  return 0


Iustin Pop's avatar
Iustin Pop committed
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
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

  op = opcodes.OpMoveInstance(instance_name=instance_name,
888
                              target_node=opts.node,
889
                              shutdown_timeout=opts.shutdown_timeout)
Iustin Pop's avatar
Iustin Pop committed
890
891
892
893
  SubmitOrSend(op, opts, cl=cl)
  return 0


Iustin Pop's avatar
Iustin Pop committed
894
895
896
def ConnectToInstanceConsole(opts, args):
  """Connect to the console of an instance.

897
898
899
900
901
  @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
902
903
904
905
906

  """
  instance_name = args[0]

  op = opcodes.OpConnectConsole(instance_name=instance_name)
907
  cmd = SubmitOpCode(op)
908
909

  if opts.show_command:
910
    ToStdout("%s", utils.ShellQuoteArgs(cmd))
911
912
913
914
  else:
    try:
      os.execvp(cmd[0], cmd)
    finally:
915
      ToStderr("Can't run console command %s with arguments:\n'%s'",
916
               cmd[0], " ".join(cmd))
Iustin Pop's avatar
Iustin Pop committed
917
      os._exit(1) # pylint: disable-msg=W0212
Iustin Pop's avatar
Iustin Pop committed
918
919


920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
def _FormatLogicalID(dev_type, logical_id):
  """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 = [
      ("nodeA", "%s, minor=%s" % (node_a, minor_a)),
      ("nodeB", "%s, minor=%s" % (node_b, minor_b)),
      ("port", port),
      ("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


def _FormatBlockDevInfo(idx, top_level, dev, static):
Iustin Pop's avatar
Iustin Pop committed
942
943
  """Show block device information.

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

947
948
949
950
  @type idx: int
  @param idx: the index of the current disk
  @type top_level: boolean
  @param top_level: if this a top-level disk?
951
952
953
954
955
  @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
956
957
  @return: a list of either strings, tuples or lists
      (which should be formatted at a higher indent level)
958

Iustin Pop's avatar
Iustin Pop committed
959
  """
960
  def helper(dtype, status):
961
962
963
964
965
966
    """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}
967
    @return: the string representing the status
968
969

    """
Iustin Pop's avatar
Iustin Pop committed
970
    if not status:
971
972
      return "not active"
    txt = ""
973
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
974
975
    if major is None:
      major_string = "N/A"
Iustin Pop's avatar
Iustin Pop committed
976
    else:
977
      major_string = str(major)
978

979
980
981
982
983
984
985
986
987
988
989
    if minor is None:
      minor_string = "N/A"
    else:
      minor_string = str(minor)

    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:
          sync_text += " ETA %ds" % estt
990
        else:
991
992
993
994
995
996
997
          sync_text += " ETA unknown"
      else:
        sync_text = "in sync"
      if degr:
        degr_text = "*DEGRADED*"
      else:
        degr_text = "ok"
998
      if ldisk_status == constants.LDS_FAULTY:
999
        ldisk_text = " *MISSING DISK*"
1000
1001
      elif ldisk_status == constants.LDS_UNKNOWN:
        ldisk_text = " *UNCERTAIN STATE*"
1002
1003
1004
1005
      else:
        ldisk_text = ""
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
    elif dtype == constants.LD_LV:
1006
      if ldisk_status == constants.LDS_FAULTY:
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
        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:
      txt = "disk %d" % idx
Iustin Pop's avatar
Iustin Pop committed
1019
  else:
1020
    txt = "child %d" % idx
Iustin Pop's avatar
Iustin Pop committed
1021
1022
1023
1024
1025
  if isinstance(dev["size"], int):
    nice_size = utils.FormatUnit(dev["size"], "h")
  else:
    nice_size = dev["size"]
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1026
1027
1028
  data = []
  if top_level:
    data.append(("access mode", dev["mode"]))
Iustin Pop's avatar
Iustin Pop committed
1029
  if dev["logical_id"] is not None:
1030
1031
1032
1033
1034
1035
1036
1037
    try:
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"])
    except ValueError:
      l_id = [str(dev["logical_id"])]
    if len(l_id) == 1:
      data.append(("logical_id", l_id[0]))
    else:
      data.extend(l_id)
Iustin Pop's avatar
Iustin Pop committed
1038
  elif dev["physical_id"] is not None:
1039
1040
    data.append("physical_id:")
    data.append([dev["physical_id"]])
1041
  if not static:
1042
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1043
  if dev["sstatus"] and not static:
1044
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
Iustin Pop's avatar
Iustin Pop committed
1045
1046

  if dev["children"]:
1047
1048
1049
1050
1051
    data.append("child devices:")
    for c_idx, child in enumerate(dev["children"]):
      data.append(_FormatBlockDevInfo(c_idx, False, child, static))
  d1.append(data)
  return d1
Iustin Pop's avatar
Iustin Pop committed
1052
1053


1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
def _FormatList(buf, data, indent_level):
  """Formats a list of data at a given indent level.

  If the element of the list is:
    - a string, it is simply formatted as is
    - a tuple, it will be split into key, value and the all the
      values in a list will be aligned all at the same start column
    - a list, will be recursively formatted

  @type buf: StringIO
  @param buf: the buffer into which we write the output
  @param data: the list to format
  @type indent_level: int
  @param indent_level: the indent level to format at

  """
  max_tlen = max([len(elem[0]) for elem in data
                 if isinstance(elem, tuple)] or [0])
  for elem in data:
    if isinstance(elem, basestring):
      buf.write("%*s%s\n" % (2*indent_level, "", elem))
    elif isinstance(elem, tuple):
      key, value = elem
      spacer = "%*s" % (max_tlen - len(key), "")
      buf.write("%*s%s:%s %s\n" % (2*indent_level, "", key, spacer, value))
    elif isinstance(elem, list):
      _FormatList(buf, elem, indent_level+1)

1082

Iustin Pop's avatar
Iustin Pop committed
1083
1084
1085
def ShowInstanceConfig(opts, args):
  """Compute instance run-time status.

1086
1087
1088
1089
1090
1091
1092
  @param opts: the command line options selected by the user
  @type args: list
  @param args: either an empty list, and then we query all
      instances, or should contain a list of instance names
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
1093
  """
Guido Trotter's avatar
Guido Trotter committed
1094
1095
1096
1097
1098
1099
1100
1101
1102
  if not args and not opts.show_all:
    ToStderr("No instance selected."
             " Please pass in --all if you want to query all instances.\n"
             "Note that this can take a long time on a big cluster.")
    return 1
  elif args and opts.show_all:
    ToStderr("Cannot use --all if you specify instance names.")
    return 1

Iustin Pop's avatar
Iustin Pop committed
1103
  retcode = 0
1104
  op = opcodes.OpQueryInstanceData(instances=args, static=opts.static)
Iustin Pop's avatar
Iustin Pop committed
1105
1106
  result = SubmitOpCode(op)
  if not result:
1107
    ToStdout("No instances.")
Iustin Pop's avatar
Iustin Pop committed
1108
1109
1110
1111
1112
1113
1114
    return 1

  buf = StringIO()
  retcode = 0
  for instance_name in result:
    instance = result[instance_name]
    buf.write("Instance name: %s\n" % instance["name"])
1115
    buf.write("UUID: %s\n" % instance["uuid"])
1116
1117
1118
    buf.write("Serial number: %s\n" % instance["serial_no"])
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1119
1120
1121
1122
1123
1124
    buf.write("State: configured to be %s" % instance["config_state"])
    if not opts.static:
      buf.write(", actual state is %s" % instance["run_state"])
    buf.write("\n")
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
    ##          instance["auto_balance"])
Iustin Pop's avatar
Iustin Pop committed
1125
1126
    buf.write("  Nodes:\n")
    buf.write("    - primary: %s\n" % instance["pnode"])
1127
    buf.write("    - secondaries: %s\n" % utils.CommaJoin(instance["snodes"]))
Iustin Pop's avatar
Iustin Pop committed
1128
    buf.write("  Operating system: %s\n" % instance["os"])
1129
1130
    if instance.has_key("network_port"):
      buf.write("  Allocated network port: %s\n" % instance["network_port"])
1131
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146

    # custom VNC console information
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
                                                 None)
    if vnc_bind_address:
      port = instance["network_port"]
      display = int(port) - constants.VNC_BASE_PORT
      if display > 0 and vnc_bind_address == constants.BIND_ADDRESS_GLOBAL:
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
                                                   port,
                                                   display)
      elif display > 0 and utils.IsValidIP(vnc_bind_address):
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
                             (vnc_bind_address, port,
                              instance["pnode"], display))
1147
      else:
1148
1149
1150
        # vnc bind address is a file
        vnc_console_port = "%s:%s" % (instance["pnode"],
                                      vnc_bind_address)
1151
1152
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)

1153
    for key in instance["hv_actual"]:
1154
1155
      if key in instance["hv_instance"]:
        val = instance["hv_instance"][key]
1156
      else:
1157
        val = "default (%s)" % instance["hv_actual"][key]
1158
      buf.write("    - %s: %s\n" % (key, val))
Iustin Pop's avatar
Iustin Pop committed
1159
    buf.write("  Hardware:\n")
Iustin Pop's avatar
Iustin Pop committed
1160
1161
1162