Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ c8bde61e

History | View | Annotate | Download (59.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Instance related commands"""
22

    
23
# pylint: disable=W0401,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0614: Unused import %s from wildcard import (since we need cli)
26
# C0103: Invalid name gnt-instance
27

    
28
import itertools
29
import simplejson
30
import logging
31
from cStringIO import StringIO
32

    
33
from ganeti.cli import *
34
from ganeti import opcodes
35
from ganeti import constants
36
from ganeti import compat
37
from ganeti import utils
38
from ganeti import errors
39
from ganeti import netutils
40
from ganeti import ssh
41
from ganeti import objects
42
from ganeti import ht
43

    
44

    
45
_EXPAND_CLUSTER = "cluster"
46
_EXPAND_NODES_BOTH = "nodes"
47
_EXPAND_NODES_PRI = "nodes-pri"
48
_EXPAND_NODES_SEC = "nodes-sec"
49
_EXPAND_NODES_BOTH_BY_TAGS = "nodes-by-tags"
50
_EXPAND_NODES_PRI_BY_TAGS = "nodes-pri-by-tags"
51
_EXPAND_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
52
_EXPAND_INSTANCES = "instances"
53
_EXPAND_INSTANCES_BY_TAGS = "instances-by-tags"
54

    
55
_EXPAND_NODES_TAGS_MODES = frozenset([
56
  _EXPAND_NODES_BOTH_BY_TAGS,
57
  _EXPAND_NODES_PRI_BY_TAGS,
58
  _EXPAND_NODES_SEC_BY_TAGS,
59
  ])
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
_MISSING = object()
69
_ENV_OVERRIDE = frozenset(["list"])
70

    
71

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

75
  For _EXPAND_CLUSTER, all instances will be returned. For
76
  _EXPAND_NODES_PRI/SEC, all instances having those nodes as
77
  primary/secondary will be returned. For _EXPAND_NODES_BOTH, all
78
  instances having those nodes as either primary or secondary will be
79
  returned. For _EXPAND_INSTANCES, the given instances will be
80
  returned.
81

82
  @param mode: one of L{_EXPAND_CLUSTER}, L{_EXPAND_NODES_BOTH},
83
      L{_EXPAND_NODES_PRI}, L{_EXPAND_NODES_SEC} or
84
      L{_EXPAND_INSTANCES}
85
  @param names: a list of names; for cluster, it must be empty,
86
      and for node and instance it must be a list of valid item
87
      names (short names are valid as usual, e.g. node1 instead of
88
      node1.example.com)
89
  @rtype: list
90
  @return: the list of names after the expansion
91
  @raise errors.ProgrammerError: for unknown selection type
92
  @raise errors.OpPrereqError: for invalid input parameters
93

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

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

    
106
  elif (mode in _EXPAND_NODES_TAGS_MODES or
107
        mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_PRI, _EXPAND_NODES_SEC)):
108
    if mode in _EXPAND_NODES_TAGS_MODES:
109
      if not names:
110
        raise errors.OpPrereqError("No node tags passed", errors.ECODE_INVAL)
111
      ndata = client.QueryNodes([], ["name", "pinst_list",
112
                                     "sinst_list", "tags"], False)
113
      ndata = [row for row in ndata if set(row[3]).intersection(names)]
114
    else:
115
      if not names:
116
        raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
117
      ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
118
                              False)
119

    
120
    ipri = [row[1] for row in ndata]
121
    pri_names = list(itertools.chain(*ipri))
122
    isec = [row[2] for row in ndata]
123
    sec_names = list(itertools.chain(*isec))
124
    if mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_BOTH_BY_TAGS):
125
      inames = pri_names + sec_names
126
    elif mode in (_EXPAND_NODES_PRI, _EXPAND_NODES_PRI_BY_TAGS):
127
      inames = pri_names
128
    elif mode in (_EXPAND_NODES_SEC, _EXPAND_NODES_SEC_BY_TAGS):
129
      inames = sec_names
130
    else:
131
      raise errors.ProgrammerError("Unhandled shutdown type")
132
  elif mode == _EXPAND_INSTANCES:
133
    if not names:
134
      raise errors.OpPrereqError("No instance names passed",
135
                                 errors.ECODE_INVAL)
136
    idata = client.QueryInstances(names, ["name"], False)
137
    inames = [row[0] for row in idata]
138
  elif mode == _EXPAND_INSTANCES_BY_TAGS:
139
    if not names:
140
      raise errors.OpPrereqError("No instance tags passed",
141
                                 errors.ECODE_INVAL)
142
    idata = client.QueryInstances([], ["name", "tags"], False)
143
    inames = [row[0] for row in idata if set(row[1]).intersection(names)]
144
  else:
145
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
146

    
147
  return inames
148

    
149

    
150
def _EnsureInstancesExist(client, names):
151
  """Check for and ensure the given instance names exist.
152

153
  This function will raise an OpPrereqError in case they don't
154
  exist. Otherwise it will exit cleanly.
155

156
  @type client: L{ganeti.luxi.Client}
157
  @param client: the client to use for the query
158
  @type names: list
159
  @param names: the list of instance names to query
160
  @raise errors.OpPrereqError: in case any instance is missing
161

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

    
171

    
172
def GenericManyOps(operation, fn):
173
  """Generic multi-instance operations.
174

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

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

    
205

    
206
def ListInstances(opts, args):
207
  """List instances and their properties.
208

209
  @param opts: the command line options selected by the user
210
  @type args: list
211
  @param args: should be an empty list
212
  @rtype: int
213
  @return: the desired exit code
214

215
  """
216
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
217

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

    
225
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
226
                     opts.separator, not opts.no_headers,
227
                     format_override=fmtoverride, verbose=opts.verbose,
228
                     force_filter=opts.force_filter)
229

    
230

    
231
def ListInstanceFields(opts, args):
232
  """List instance fields.
233

234
  @param opts: the command line options selected by the user
235
  @type args: list
236
  @param args: fields to list, or empty for all
237
  @rtype: int
238
  @return: the desired exit code
239

240
  """
241
  return GenericListFields(constants.QR_INSTANCE, args, opts.separator,
242
                           not opts.no_headers)
243

    
244

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

248
  This is just a wrapper over GenericInstanceCreate.
249

250
  """
251
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
252

    
253

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

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

260
    {"instance-name":{
261
      "disk_size": [20480],
262
      "template": "drbd",
263
      "backend": {
264
        "memory": 512,
265
        "vcpus": 1 },
266
      "os": "debootstrap",
267
      "primary_node": "firstnode",
268
      "secondary_node": "secondnode",
269
      "iallocator": "dumb"}
270
    }
271

272
  Note that I{primary_node} and I{secondary_node} have precedence over
273
  I{iallocator}.
274

275
  @param opts: the command line options selected by the user
276
  @type args: list
277
  @param args: should contain one element, the json filename
278
  @rtype: int
279
  @return: the desired exit code
280

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

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

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

    
322
    if (spec["hvparams"] and
323
        not isinstance(spec["hvparams"], dict)):
324
      raise errors.OpPrereqError("Hypervisor parameters must be a dict.",
325
                                 errors.ECODE_INVAL)
326

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

    
334
  if not isinstance(instance_data, dict):
335
    ToStderr("The instance definition file is not in dict format.")
336
    return 1
337

    
338
  jex = JobExecutor(opts=opts)
339

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

    
349
    hypervisor = specs["hypervisor"]
350
    hvparams = specs["hvparams"]
351

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

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

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

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

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

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

    
405
  return 0
406

    
407

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

411
  @param opts: the command line options selected by the user
412
  @type args: list
413
  @param args: should contain only one element, the name of the
414
      instance to be reinstalled
415
  @rtype: int
416
  @return: the desired exit code
417

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

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

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

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

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

    
446
    choices.append(("x", "exit", "Exit gnt-instance reinstall"))
447
    selected = AskUser("Enter OS template number (or x to abort):",
448
                       choices)
449

    
450
    if selected == "exit":
451
      ToStderr("User aborted reinstall, exiting")
452
      return 1
453

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

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

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

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

    
491

    
492
def RemoveInstance(opts, args):
493
  """Remove an instance.
494

495
  @param opts: the command line options selected by the user
496
  @type args: list
497
  @param args: should contain only one element, the name of
498
      the instance to be removed
499
  @rtype: int
500
  @return: the desired exit code
501

502
  """
503
  instance_name = args[0]
504
  force = opts.force
505
  cl = GetClient()
506

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

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

    
516
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
517
                                ignore_failures=opts.ignore_failures,
518
                                shutdown_timeout=opts.shutdown_timeout)
519
  SubmitOrSend(op, opts, cl=cl)
520
  return 0
521

    
522

    
523
def RenameInstance(opts, args):
524
  """Rename an instance.
525

526
  @param opts: the command line options selected by the user
527
  @type args: list
528
  @param args: should contain two elements, the old and the
529
      new instance names
530
  @rtype: int
531
  @return: the desired exit code
532

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

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

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

    
548
  return 0
549

    
550

    
551
def ActivateDisks(opts, args):
552
  """Activate an instance's disks.
553

554
  This serves two purposes:
555
    - it allows (as long as the instance is not running)
556
      mounting the disks and modifying them from the node
557
    - it repairs inactive secondary drbds
558

559
  @param opts: the command line options selected by the user
560
  @type args: list
561
  @param args: should contain only one element, the instance name
562
  @rtype: int
563
  @return: the desired exit code
564

565
  """
566
  instance_name = args[0]
567
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
568
                                       ignore_size=opts.ignore_size)
569
  disks_info = SubmitOrSend(op, opts)
570
  for host, iname, nname in disks_info:
571
    ToStdout("%s:%s:%s", host, iname, nname)
572
  return 0
573

    
574

    
575
def DeactivateDisks(opts, args):
576
  """Deactivate an instance's disks.
577

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

581
  @param opts: the command line options selected by the user
582
  @type args: list
583
  @param args: should contain only one element, the instance name
584
  @rtype: int
585
  @return: the desired exit code
586

587
  """
588
  instance_name = args[0]
589
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
590
                                         force=opts.force)
591
  SubmitOrSend(op, opts)
592
  return 0
593

    
594

    
595
def RecreateDisks(opts, args):
596
  """Recreate an instance's disks.
597

598
  @param opts: the command line options selected by the user
599
  @type args: list
600
  @param args: should contain only one element, the instance name
601
  @rtype: int
602
  @return: the desired exit code
603

604
  """
605
  instance_name = args[0]
606

    
607
  disks = []
608

    
609
  if opts.disks:
610
    for didx, ddict in opts.disks:
611
      didx = int(didx)
612

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

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

    
625
      disks.append((didx, ddict))
626

    
627
    # TODO: Verify modifyable parameters (already done in
628
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
629

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

    
638
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
639
                                       disks=disks, nodes=nodes)
640
  SubmitOrSend(op, opts)
641

    
642
  return 0
643

    
644

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

648
  @param opts: the command line options selected by the user
649
  @type args: list
650
  @param args: should contain three elements, the target instance name,
651
      the target disk id, and the target growth
652
  @rtype: int
653
  @return: the desired exit code
654

655
  """
656
  instance = args[0]
657
  disk = args[1]
658
  try:
659
    disk = int(disk)
660
  except (TypeError, ValueError), err:
661
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
662
                               errors.ECODE_INVAL)
663
  try:
664
    amount = utils.ParseUnit(args[2])
665
  except errors.UnitParseError:
666
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
667
                               errors.ECODE_INVAL)
668
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
669
                                  disk=disk, amount=amount,
670
                                  wait_for_sync=opts.wait_for_sync,
671
                                  absolute=opts.absolute)
672
  SubmitOrSend(op, opts)
673
  return 0
674

    
675

    
676
def _StartupInstance(name, opts):
677
  """Startup instances.
678

679
  This returns the opcode to start an instance, and its decorator will
680
  wrap this into a loop starting all desired instances.
681

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

686
  """
687
  op = opcodes.OpInstanceStartup(instance_name=name,
688
                                 force=opts.force,
689
                                 ignore_offline_nodes=opts.ignore_offline,
690
                                 no_remember=opts.no_remember,
691
                                 startup_paused=opts.startup_paused)
692
  # do not add these parameters to the opcode unless they're defined
693
  if opts.hvparams:
694
    op.hvparams = opts.hvparams
695
  if opts.beparams:
696
    op.beparams = opts.beparams
697
  return op
698

    
699

    
700
def _RebootInstance(name, opts):
701
  """Reboot instance(s).
702

703
  This returns the opcode to reboot an instance, and its decorator
704
  will wrap this into a loop rebooting all desired instances.
705

706
  @param name: the name of the instance to act on
707
  @param opts: the command line options selected by the user
708
  @return: the opcode needed for the operation
709

710
  """
711
  return opcodes.OpInstanceReboot(instance_name=name,
712
                                  reboot_type=opts.reboot_type,
713
                                  ignore_secondaries=opts.ignore_secondaries,
714
                                  shutdown_timeout=opts.shutdown_timeout)
715

    
716

    
717
def _ShutdownInstance(name, opts):
718
  """Shutdown an instance.
719

720
  This returns the opcode to shutdown an instance, and its decorator
721
  will wrap this into a loop shutting down 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.OpInstanceShutdown(instance_name=name,
729
                                    timeout=opts.timeout,
730
                                    ignore_offline_nodes=opts.ignore_offline,
731
                                    no_remember=opts.no_remember)
732

    
733

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

737
  @param opts: the command line options selected by the user
738
  @type args: list
739
  @param args: should contain only one element, the instance name
740
  @rtype: int
741
  @return: the desired exit code
742

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

    
772
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
773
                                      remote_node=new_2ndary, mode=mode,
774
                                      iallocator=iallocator,
775
                                      early_release=opts.early_release,
776
                                      ignore_ipolicy=opts.ignore_ipolicy)
777
  SubmitOrSend(op, opts)
778
  return 0
779

    
780

    
781
def FailoverInstance(opts, args):
782
  """Failover an instance.
783

784
  The failover is done by shutting it down on its present node and
785
  starting it on the secondary.
786

787
  @param opts: the command line options selected by the user
788
  @type args: list
789
  @param args: should contain only one element, the instance name
790
  @rtype: int
791
  @return: the desired exit code
792

793
  """
794
  cl = GetClient()
795
  instance_name = args[0]
796
  force = opts.force
797
  iallocator = opts.iallocator
798
  target_node = opts.dst_node
799

    
800
  if iallocator and target_node:
801
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
802
                               " node (-n) but not both", errors.ECODE_INVAL)
803

    
804
  if not force:
805
    _EnsureInstancesExist(cl, [instance_name])
806

    
807
    usertext = ("Failover will happen to image %s."
808
                " This requires a shutdown of the instance. Continue?" %
809
                (instance_name,))
810
    if not AskUser(usertext):
811
      return 1
812

    
813
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
814
                                  ignore_consistency=opts.ignore_consistency,
815
                                  shutdown_timeout=opts.shutdown_timeout,
816
                                  iallocator=iallocator,
817
                                  target_node=target_node,
818
                                  ignore_ipolicy=opts.ignore_ipolicy)
819
  SubmitOrSend(op, opts, cl=cl)
820
  return 0
821

    
822

    
823
def MigrateInstance(opts, args):
824
  """Migrate an instance.
825

826
  The migrate is done without shutdown.
827

828
  @param opts: the command line options selected by the user
829
  @type args: list
830
  @param args: should contain only one element, the instance name
831
  @rtype: int
832
  @return: the desired exit code
833

834
  """
835
  cl = GetClient()
836
  instance_name = args[0]
837
  force = opts.force
838
  iallocator = opts.iallocator
839
  target_node = opts.dst_node
840

    
841
  if iallocator and target_node:
842
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
843
                               " node (-n) but not both", errors.ECODE_INVAL)
844

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

    
848
    if opts.cleanup:
849
      usertext = ("Instance %s will be recovered from a failed migration."
850
                  " Note that the migration procedure (including cleanup)" %
851
                  (instance_name,))
852
    else:
853
      usertext = ("Instance %s will be migrated. Note that migration" %
854
                  (instance_name,))
855
    usertext += (" might impact the instance if anything goes wrong"
856
                 " (e.g. due to bugs in the hypervisor). Continue?")
857
    if not AskUser(usertext):
858
      return 1
859

    
860
  # this should be removed once --non-live is deprecated
861
  if not opts.live and opts.migration_mode is not None:
862
    raise errors.OpPrereqError("Only one of the --non-live and "
863
                               "--migration-mode options can be passed",
864
                               errors.ECODE_INVAL)
865
  if not opts.live: # --non-live passed
866
    mode = constants.HT_MIGRATION_NONLIVE
867
  else:
868
    mode = opts.migration_mode
869

    
870
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
871
                                 cleanup=opts.cleanup, iallocator=iallocator,
872
                                 target_node=target_node,
873
                                 allow_failover=opts.allow_failover,
874
                                 allow_runtime_changes=opts.allow_runtime_chgs,
875
                                 ignore_ipolicy=opts.ignore_ipolicy)
876
  SubmitOrSend(op, cl=cl, opts=opts)
877
  return 0
878

    
879

    
880
def MoveInstance(opts, args):
881
  """Move an instance.
882

883
  @param opts: the command line options selected by the user
884
  @type args: list
885
  @param args: should contain only one element, the instance name
886
  @rtype: int
887
  @return: the desired exit code
888

889
  """
890
  cl = GetClient()
891
  instance_name = args[0]
892
  force = opts.force
893

    
894
  if not force:
895
    usertext = ("Instance %s will be moved."
896
                " This requires a shutdown of the instance. Continue?" %
897
                (instance_name,))
898
    if not AskUser(usertext):
899
      return 1
900

    
901
  op = opcodes.OpInstanceMove(instance_name=instance_name,
902
                              target_node=opts.node,
903
                              shutdown_timeout=opts.shutdown_timeout,
904
                              ignore_consistency=opts.ignore_consistency,
905
                              ignore_ipolicy=opts.ignore_ipolicy)
906
  SubmitOrSend(op, opts, cl=cl)
907
  return 0
908

    
909

    
910
def ConnectToInstanceConsole(opts, args):
911
  """Connect to the console of an instance.
912

913
  @param opts: the command line options selected by the user
914
  @type args: list
915
  @param args: should contain only one element, the instance name
916
  @rtype: int
917
  @return: the desired exit code
918

919
  """
920
  instance_name = args[0]
921

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

    
931
  del cl
932

    
933
  if not console_data:
934
    if oper_state:
935
      # Instance is running
936
      raise errors.OpExecError("Console information for instance %s is"
937
                               " unavailable" % instance_name)
938
    else:
939
      raise errors.OpExecError("Instance %s is not running, can't get console" %
940
                               instance_name)
941

    
942
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
943
                    opts.show_command, cluster_name)
944

    
945

    
946
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
947
               _runcmd_fn=utils.RunCmd):
948
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
949

950
  @type console: L{objects.InstanceConsole}
951
  @param console: Console object
952
  @type show_command: bool
953
  @param show_command: Whether to just display commands
954
  @type cluster_name: string
955
  @param cluster_name: Cluster name as retrieved from master daemon
956

957
  """
958
  assert console.Validate()
959

    
960
  if console.kind == constants.CONS_MESSAGE:
961
    feedback_fn(console.message)
962
  elif console.kind == constants.CONS_VNC:
963
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
964
                " URL <vnc://%s:%s/>",
965
                console.instance, console.host, console.port,
966
                console.display, console.host, console.port)
967
  elif console.kind == constants.CONS_SPICE:
968
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
969
                console.host, console.port)
970
  elif console.kind == constants.CONS_SSH:
971
    # Convert to string if not already one
972
    if isinstance(console.command, basestring):
973
      cmd = console.command
974
    else:
975
      cmd = utils.ShellQuoteArgs(console.command)
976

    
977
    srun = ssh.SshRunner(cluster_name=cluster_name)
978
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
979
                            batch=True, quiet=False, tty=True)
980

    
981
    if show_command:
982
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
983
    else:
984
      result = _runcmd_fn(ssh_cmd, interactive=True)
985
      if result.failed:
986
        logging.error("Console command \"%s\" failed with reason '%s' and"
987
                      " output %r", result.cmd, result.fail_reason,
988
                      result.output)
989
        raise errors.OpExecError("Connection to console of instance %s failed,"
990
                                 " please check cluster configuration" %
991
                                 console.instance)
992
  else:
993
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
994

    
995
  return constants.EXIT_SUCCESS
996

    
997

    
998
def _FormatLogicalID(dev_type, logical_id, roman):
999
  """Formats the logical_id of a disk.
1000

1001
  """
1002
  if dev_type == constants.LD_DRBD8:
1003
    node_a, node_b, port, minor_a, minor_b, key = logical_id
1004
    data = [
1005
      ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
1006
                                                            convert=roman))),
1007
      ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
1008
                                                            convert=roman))),
1009
      ("port", compat.TryToRoman(port, convert=roman)),
1010
      ("auth key", key),
1011
      ]
1012
  elif dev_type == constants.LD_LV:
1013
    vg_name, lv_name = logical_id
1014
    data = ["%s/%s" % (vg_name, lv_name)]
1015
  else:
1016
    data = [str(logical_id)]
1017

    
1018
  return data
1019

    
1020

    
1021
def _FormatBlockDevInfo(idx, top_level, dev, roman):
1022
  """Show block device information.
1023

1024
  This is only used by L{ShowInstanceConfig}, but it's too big to be
1025
  left for an inline definition.
1026

1027
  @type idx: int
1028
  @param idx: the index of the current disk
1029
  @type top_level: boolean
1030
  @param top_level: if this a top-level disk?
1031
  @type dev: dict
1032
  @param dev: dictionary with disk information
1033
  @type roman: boolean
1034
  @param roman: whether to try to use roman integers
1035
  @return: a list of either strings, tuples or lists
1036
      (which should be formatted at a higher indent level)
1037

1038
  """
1039
  def helper(dtype, status):
1040
    """Format one line for physical device status.
1041

1042
    @type dtype: str
1043
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
1044
    @type status: tuple
1045
    @param status: a tuple as returned from L{backend.FindBlockDevice}
1046
    @return: the string representing the status
1047

1048
    """
1049
    if not status:
1050
      return "not active"
1051
    txt = ""
1052
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
1053
    if major is None:
1054
      major_string = "N/A"
1055
    else:
1056
      major_string = str(compat.TryToRoman(major, convert=roman))
1057

    
1058
    if minor is None:
1059
      minor_string = "N/A"
1060
    else:
1061
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1062

    
1063
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
1064
    if dtype in (constants.LD_DRBD8, ):
1065
      if syncp is not None:
1066
        sync_text = "*RECOVERING* %5.2f%%," % syncp
1067
        if estt:
1068
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1069
        else:
1070
          sync_text += " ETA unknown"
1071
      else:
1072
        sync_text = "in sync"
1073
      if degr:
1074
        degr_text = "*DEGRADED*"
1075
      else:
1076
        degr_text = "ok"
1077
      if ldisk_status == constants.LDS_FAULTY:
1078
        ldisk_text = " *MISSING DISK*"
1079
      elif ldisk_status == constants.LDS_UNKNOWN:
1080
        ldisk_text = " *UNCERTAIN STATE*"
1081
      else:
1082
        ldisk_text = ""
1083
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1084
    elif dtype == constants.LD_LV:
1085
      if ldisk_status == constants.LDS_FAULTY:
1086
        ldisk_text = " *FAILED* (failed drive?)"
1087
      else:
1088
        ldisk_text = ""
1089
      txt += ldisk_text
1090
    return txt
1091

    
1092
  # the header
1093
  if top_level:
1094
    if dev["iv_name"] is not None:
1095
      txt = dev["iv_name"]
1096
    else:
1097
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1098
  else:
1099
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1100
  if isinstance(dev["size"], int):
1101
    nice_size = utils.FormatUnit(dev["size"], "h")
1102
  else:
1103
    nice_size = dev["size"]
1104
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1105
  data = []
1106
  if top_level:
1107
    data.append(("access mode", dev["mode"]))
1108
  if dev["logical_id"] is not None:
1109
    try:
1110
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1111
    except ValueError:
1112
      l_id = [str(dev["logical_id"])]
1113
    if len(l_id) == 1:
1114
      data.append(("logical_id", l_id[0]))
1115
    else:
1116
      data.extend(l_id)
1117
  elif dev["physical_id"] is not None:
1118
    data.append("physical_id:")
1119
    data.append([dev["physical_id"]])
1120

    
1121
  if dev["pstatus"]:
1122
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1123

    
1124
  if dev["sstatus"]:
1125
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1126

    
1127
  if dev["children"]:
1128
    data.append("child devices:")
1129
    for c_idx, child in enumerate(dev["children"]):
1130
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1131
  d1.append(data)
1132
  return d1
1133

    
1134

    
1135
def _FormatList(buf, data, indent_level):
1136
  """Formats a list of data at a given indent level.
1137

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

1144
  @type buf: StringIO
1145
  @param buf: the buffer into which we write the output
1146
  @param data: the list to format
1147
  @type indent_level: int
1148
  @param indent_level: the indent level to format at
1149

1150
  """
1151
  max_tlen = max([len(elem[0]) for elem in data
1152
                 if isinstance(elem, tuple)] or [0])
1153
  for elem in data:
1154
    if isinstance(elem, basestring):
1155
      buf.write("%*s%s\n" % (2 * indent_level, "", elem))
1156
    elif isinstance(elem, tuple):
1157
      key, value = elem
1158
      spacer = "%*s" % (max_tlen - len(key), "")
1159
      buf.write("%*s%s:%s %s\n" % (2 * indent_level, "", key, spacer, value))
1160
    elif isinstance(elem, list):
1161
      _FormatList(buf, elem, indent_level + 1)
1162

    
1163

    
1164
def ShowInstanceConfig(opts, args):
1165
  """Compute instance run-time status.
1166

1167
  @param opts: the command line options selected by the user
1168
  @type args: list
1169
  @param args: either an empty list, and then we query all
1170
      instances, or should contain a list of instance names
1171
  @rtype: int
1172
  @return: the desired exit code
1173

1174
  """
1175
  if not args and not opts.show_all:
1176
    ToStderr("No instance selected."
1177
             " Please pass in --all if you want to query all instances.\n"
1178
             "Note that this can take a long time on a big cluster.")
1179
    return 1
1180
  elif args and opts.show_all:
1181
    ToStderr("Cannot use --all if you specify instance names.")
1182
    return 1
1183

    
1184
  retcode = 0
1185
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1186
                                   use_locking=not opts.static)
1187
  result = SubmitOpCode(op, opts=opts)
1188
  if not result:
1189
    ToStdout("No instances.")
1190
    return 1
1191

    
1192
  buf = StringIO()
1193
  retcode = 0
1194
  for instance_name in result:
1195
    instance = result[instance_name]
1196
    buf.write("Instance name: %s\n" % instance["name"])
1197
    buf.write("UUID: %s\n" % instance["uuid"])
1198
    buf.write("Serial number: %s\n" %
1199
              compat.TryToRoman(instance["serial_no"],
1200
                                convert=opts.roman_integers))
1201
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
1202
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1203
    buf.write("State: configured to be %s" % instance["config_state"])
1204
    if instance["run_state"]:
1205
      buf.write(", actual state is %s" % instance["run_state"])
1206
    buf.write("\n")
1207
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
1208
    ##          instance["auto_balance"])
1209
    buf.write("  Nodes:\n")
1210
    buf.write("    - primary: %s\n" % instance["pnode"])
1211
    buf.write("      group: %s (UUID %s)\n" %
1212
              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1213
    buf.write("    - secondaries: %s\n" %
1214
              utils.CommaJoin("%s (group %s, group UUID %s)" %
1215
                                (name, group_name, group_uuid)
1216
                              for (name, group_name, group_uuid) in
1217
                                zip(instance["snodes"],
1218
                                    instance["snodes_group_names"],
1219
                                    instance["snodes_group_uuids"])))
1220
    buf.write("  Operating system: %s\n" % instance["os"])
1221
    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
1222
                        level=2)
1223
    if "network_port" in instance:
1224
      buf.write("  Allocated network port: %s\n" %
1225
                compat.TryToRoman(instance["network_port"],
1226
                                  convert=opts.roman_integers))
1227
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1228

    
1229
    # custom VNC console information
1230
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1231
                                                 None)
1232
    if vnc_bind_address:
1233
      port = instance["network_port"]
1234
      display = int(port) - constants.VNC_BASE_PORT
1235
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1236
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1237
                                                   port,
1238
                                                   display)
1239
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1240
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1241
                             (vnc_bind_address, port,
1242
                              instance["pnode"], display))
1243
      else:
1244
        # vnc bind address is a file
1245
        vnc_console_port = "%s:%s" % (instance["pnode"],
1246
                                      vnc_bind_address)
1247
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1248

    
1249
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1250
                        level=2)
1251
    buf.write("  Hardware:\n")
1252
    buf.write("    - VCPUs: %s\n" %
1253
              compat.TryToRoman(instance["be_actual"][constants.BE_VCPUS],
1254
                                convert=opts.roman_integers))
1255
    buf.write("    - maxmem: %sMiB\n" %
1256
              compat.TryToRoman(instance["be_actual"][constants.BE_MAXMEM],
1257
                                convert=opts.roman_integers))
1258
    buf.write("    - minmem: %sMiB\n" %
1259
              compat.TryToRoman(instance["be_actual"][constants.BE_MINMEM],
1260
                                convert=opts.roman_integers))
1261
    # deprecated "memory" value, kept for one version for compatibility
1262
    # TODO(ganeti 2.7) remove.
1263
    buf.write("    - memory: %sMiB\n" %
1264
              compat.TryToRoman(instance["be_actual"][constants.BE_MAXMEM],
1265
                                convert=opts.roman_integers))
1266
    buf.write("    - %s: %s\n" %
1267
              (constants.BE_ALWAYS_FAILOVER,
1268
               instance["be_actual"][constants.BE_ALWAYS_FAILOVER]))
1269
    buf.write("    - NICs:\n")
1270
    for idx, (ip, mac, mode, link) in enumerate(instance["nics"]):
1271
      buf.write("      - nic/%d: MAC: %s, IP: %s, mode: %s, link: %s\n" %
1272
                (idx, mac, ip, mode, link))
1273
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1274
    buf.write("  Disks:\n")
1275

    
1276
    for idx, device in enumerate(instance["disks"]):
1277
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1278
                  opts.roman_integers), 2)
1279

    
1280
  ToStdout(buf.getvalue().rstrip("\n"))
1281
  return retcode
1282

    
1283

    
1284
def _ConvertNicDiskModifications(mods):
1285
  """Converts NIC/disk modifications from CLI to opcode.
1286

1287
  When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
1288
  disks at arbitrary indices, its parameter format changed. This function
1289
  converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
1290
  newer format and adds support for new-style requests (e.g. "--new 4:add").
1291

1292
  @type mods: list of tuples
1293
  @param mods: Modifications as given by command line parser
1294
  @rtype: list of tuples
1295
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1296

1297
  """
1298
  result = []
1299

    
1300
  for (idx, params) in mods:
1301
    if idx == constants.DDM_ADD:
1302
      # Add item as last item (legacy interface)
1303
      action = constants.DDM_ADD
1304
      idxno = -1
1305
    elif idx == constants.DDM_REMOVE:
1306
      # Remove last item (legacy interface)
1307
      action = constants.DDM_REMOVE
1308
      idxno = -1
1309
    else:
1310
      # Modifications and adding/removing at arbitrary indices
1311
      try:
1312
        idxno = int(idx)
1313
      except (TypeError, ValueError):
1314
        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
1315
                                   errors.ECODE_INVAL)
1316

    
1317
      add = params.pop(constants.DDM_ADD, _MISSING)
1318
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1319

    
1320
      if not (add is _MISSING or remove is _MISSING):
1321
        raise errors.OpPrereqError("Cannot add and remove at the same time",
1322
                                   errors.ECODE_INVAL)
1323
      elif add is not _MISSING:
1324
        action = constants.DDM_ADD
1325
      elif remove is not _MISSING:
1326
        action = constants.DDM_REMOVE
1327
      else:
1328
        action = constants.DDM_MODIFY
1329

    
1330
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1331

    
1332
    if action == constants.DDM_REMOVE and params:
1333
      raise errors.OpPrereqError("Not accepting parameters on removal",
1334
                                 errors.ECODE_INVAL)
1335

    
1336
    result.append((action, idxno, params))
1337

    
1338
  return result
1339

    
1340

    
1341
def _ParseDiskSizes(mods):
1342
  """Parses disk sizes in parameters.
1343

1344
  """
1345
  for (action, _, params) in mods:
1346
    if params and constants.IDISK_SIZE in params:
1347
      params[constants.IDISK_SIZE] = \
1348
        utils.ParseUnit(params[constants.IDISK_SIZE])
1349
    elif action == constants.DDM_ADD:
1350
      raise errors.OpPrereqError("Missing required parameter 'size'",
1351
                                 errors.ECODE_INVAL)
1352

    
1353
  return mods
1354

    
1355

    
1356
def SetInstanceParams(opts, args):
1357
  """Modifies an instance.
1358

1359
  All parameters take effect only at the next restart of the instance.
1360

1361
  @param opts: the command line options selected by the user
1362
  @type args: list
1363
  @param args: should contain only one element, the instance name
1364
  @rtype: int
1365
  @return: the desired exit code
1366

1367
  """
1368
  if not (opts.nics or opts.disks or opts.disk_template or
1369
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1370
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1371
    ToStderr("Please give at least one of the parameters.")
1372
    return 1
1373

    
1374
  for param in opts.beparams:
1375
    if isinstance(opts.beparams[param], basestring):
1376
      if opts.beparams[param].lower() == "default":
1377
        opts.beparams[param] = constants.VALUE_DEFAULT
1378

    
1379
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1380
                      allowed_values=[constants.VALUE_DEFAULT])
1381

    
1382
  for param in opts.hvparams:
1383
    if isinstance(opts.hvparams[param], basestring):
1384
      if opts.hvparams[param].lower() == "default":
1385
        opts.hvparams[param] = constants.VALUE_DEFAULT
1386

    
1387
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1388
                      allowed_values=[constants.VALUE_DEFAULT])
1389

    
1390
  nics = _ConvertNicDiskModifications(opts.nics)
1391
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1392

    
1393
  if (opts.disk_template and
1394
      opts.disk_template in constants.DTS_INT_MIRROR and
1395
      not opts.node):
1396
    ToStderr("Changing the disk template to a mirrored one requires"
1397
             " specifying a secondary node")
1398
    return 1
1399

    
1400
  if opts.offline_inst:
1401
    offline = True
1402
  elif opts.online_inst:
1403
    offline = False
1404
  else:
1405
    offline = None
1406

    
1407
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1408
                                   nics=nics,
1409
                                   disks=disks,
1410
                                   disk_template=opts.disk_template,
1411
                                   remote_node=opts.node,
1412
                                   hvparams=opts.hvparams,
1413
                                   beparams=opts.beparams,
1414
                                   runtime_mem=opts.runtime_mem,
1415
                                   os_name=opts.os,
1416
                                   osparams=opts.osparams,
1417
                                   force_variant=opts.force_variant,
1418
                                   force=opts.force,
1419
                                   wait_for_sync=opts.wait_for_sync,
1420
                                   offline=offline,
1421
                                   ignore_ipolicy=opts.ignore_ipolicy)
1422

    
1423
  # even if here we process the result, we allow submit only
1424
  result = SubmitOrSend(op, opts)
1425

    
1426
  if result:
1427
    ToStdout("Modified instance %s", args[0])
1428
    for param, data in result:
1429
      ToStdout(" - %-5s -> %s", param, data)
1430
    ToStdout("Please don't forget that most parameters take effect"
1431
             " only at the next start of the instance.")
1432
  return 0
1433

    
1434

    
1435
def ChangeGroup(opts, args):
1436
  """Moves an instance to another group.
1437

1438
  """
1439
  (instance_name, ) = args
1440

    
1441
  cl = GetClient()
1442

    
1443
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1444
                                     iallocator=opts.iallocator,
1445
                                     target_groups=opts.to,
1446
                                     early_release=opts.early_release)
1447
  result = SubmitOrSend(op, opts, cl=cl)
1448

    
1449
  # Keep track of submitted jobs
1450
  jex = JobExecutor(cl=cl, opts=opts)
1451

    
1452
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1453
    jex.AddJobId(None, status, job_id)
1454

    
1455
  results = jex.GetResults()
1456
  bad_cnt = len([row for row in results if not row[0]])
1457
  if bad_cnt == 0:
1458
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1459
    rcode = constants.EXIT_SUCCESS
1460
  else:
1461
    ToStdout("There were %s errors while changing group of instance '%s'.",
1462
             bad_cnt, instance_name)
1463
    rcode = constants.EXIT_FAILURE
1464

    
1465
  return rcode
1466

    
1467

    
1468
# multi-instance selection options
1469
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1470
                           help="Do not ask for confirmation when more than"
1471
                           " one instance is affected",
1472
                           action="store_true", default=False)
1473

    
1474
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1475
                            help="Filter by nodes (primary only)",
1476
                            const=_EXPAND_NODES_PRI, action="store_const")
1477

    
1478
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1479
                            help="Filter by nodes (secondary only)",
1480
                            const=_EXPAND_NODES_SEC, action="store_const")
1481

    
1482
m_node_opt = cli_option("--node", dest="multi_mode",
1483
                        help="Filter by nodes (primary and secondary)",
1484
                        const=_EXPAND_NODES_BOTH, action="store_const")
1485

    
1486
m_clust_opt = cli_option("--all", dest="multi_mode",
1487
                         help="Select all instances in the cluster",
1488
                         const=_EXPAND_CLUSTER, action="store_const")
1489

    
1490
m_inst_opt = cli_option("--instance", dest="multi_mode",
1491
                        help="Filter by instance name [default]",
1492
                        const=_EXPAND_INSTANCES, action="store_const")
1493

    
1494
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1495
                             help="Filter by node tag",
1496
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1497
                             action="store_const")
1498

    
1499
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1500
                                 help="Filter by primary node tag",
1501
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1502
                                 action="store_const")
1503

    
1504
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1505
                                 help="Filter by secondary node tag",
1506
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1507
                                 action="store_const")
1508

    
1509
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1510
                             help="Filter by instance tag",
1511
                             const=_EXPAND_INSTANCES_BY_TAGS,
1512
                             action="store_const")
1513

    
1514
# this is defined separately due to readability only
1515
add_opts = [
1516
  NOSTART_OPT,
1517
  OS_OPT,
1518
  FORCE_VARIANT_OPT,
1519
  NO_INSTALL_OPT,
1520
  IGNORE_IPOLICY_OPT,
1521
  ]
1522

    
1523
commands = {
1524
  "add": (
1525
    AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
1526
    "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1527
    "Creates and adds a new instance to the cluster"),
1528
  "batch-create": (
1529
    BatchCreate, [ArgFile(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT],
1530
    "<instances.json>",
1531
    "Create a bunch of instances based on specs in the file."),
1532
  "console": (
1533
    ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1534
    [SHOWCMD_OPT, PRIORITY_OPT],
1535
    "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1536
  "failover": (
1537
    FailoverInstance, ARGS_ONE_INSTANCE,
1538
    [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
1539
     DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1540
     IGNORE_IPOLICY_OPT],
1541
    "[-f] <instance>", "Stops the instance, changes its primary node and"
1542
    " (if it was originally running) starts it on the new node"
1543
    " (the secondary for mirrored instances or any node"
1544
    " for shared storage)."),
1545
  "migrate": (
1546
    MigrateInstance, ARGS_ONE_INSTANCE,
1547
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1548
     PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1549
     IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1550
    "[-f] <instance>", "Migrate instance to its secondary node"
1551
    " (only for mirrored instances)"),
1552
  "move": (
1553
    MoveInstance, ARGS_ONE_INSTANCE,
1554
    [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
1555
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT, IGNORE_IPOLICY_OPT],
1556
    "[-f] <instance>", "Move instance to an arbitrary node"
1557
    " (only for instances of type file and lv)"),
1558
  "info": (
1559
    ShowInstanceConfig, ARGS_MANY_INSTANCES,
1560
    [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1561
    "[-s] {--all | <instance>...}",
1562
    "Show information on the specified instance(s)"),
1563
  "list": (
1564
    ListInstances, ARGS_MANY_INSTANCES,
1565
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1566
     FORCE_FILTER_OPT],
1567
    "[<instance>...]",
1568
    "Lists the instances and their status. The available fields can be shown"
1569
    " using the \"list-fields\" command (see the man page for details)."
1570
    " The default field list is (in order): %s." %
1571
    utils.CommaJoin(_LIST_DEF_FIELDS),
1572
    ),
1573
  "list-fields": (
1574
    ListInstanceFields, [ArgUnknown()],
1575
    [NOHDR_OPT, SEP_OPT],
1576
    "[fields...]",
1577
    "Lists all available fields for instances"),
1578
  "reinstall": (
1579
    ReinstallInstance, [ArgInstance()],
1580
    [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1581
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1582
     m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1583
     SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1584
    "[-f] <instance>", "Reinstall a stopped instance"),
1585
  "remove": (
1586
    RemoveInstance, ARGS_ONE_INSTANCE,
1587
    [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
1588
     DRY_RUN_OPT, PRIORITY_OPT],
1589
    "[-f] <instance>", "Shuts down the instance and removes it"),
1590
  "rename": (
1591
    RenameInstance,
1592
    [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1593
    [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1594
    "<instance> <new_name>", "Rename the instance"),
1595
  "replace-disks": (
1596
    ReplaceDisks, ARGS_ONE_INSTANCE,
1597
    [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1598
     NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
1599
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1600
    "[-s|-p|-n NODE|-I NAME] <instance>",
1601
    "Replaces all disks for the instance"),
1602
  "modify": (
1603
    SetInstanceParams, ARGS_ONE_INSTANCE,
1604
    [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
1605
     DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1606
     OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
1607
     ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT],
1608
    "<instance>", "Alters the parameters of an instance"),
1609
  "shutdown": (
1610
    GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1611
    [m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1612
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1613
     m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
1614
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1615
    "<instance>", "Stops an instance"),
1616
  "startup": (
1617
    GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1618
    [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1619
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1620
     m_inst_tags_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, HVOPTS_OPT,
1621
     BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1622
     NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1623
    "<instance>", "Starts an instance"),
1624
  "reboot": (
1625
    GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1626
    [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1627
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
1628
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1629
     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1630
    "<instance>", "Reboots an instance"),
1631
  "activate-disks": (
1632
    ActivateDisks, ARGS_ONE_INSTANCE,
1633
    [SUBMIT_OPT, IGNORE_SIZE_OPT, PRIORITY_OPT],
1634
    "<instance>", "Activate an instance's disks"),
1635
  "deactivate-disks": (
1636
    DeactivateDisks, ARGS_ONE_INSTANCE,
1637
    [FORCE_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1638
    "[-f] <instance>", "Deactivate an instance's disks"),
1639
  "recreate-disks": (
1640
    RecreateDisks, ARGS_ONE_INSTANCE,
1641
    [SUBMIT_OPT, DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1642
    "<instance>", "Recreate an instance's disks"),
1643
  "grow-disk": (
1644
    GrowDisk,
1645
    [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1646
     ArgUnknown(min=1, max=1)],
1647
    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
1648
    "<instance> <disk> <size>", "Grow an instance's disk"),
1649
  "change-group": (
1650
    ChangeGroup, ARGS_ONE_INSTANCE,
1651
    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT, SUBMIT_OPT],
1652
    "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1653
  "list-tags": (
1654
    ListTags, ARGS_ONE_INSTANCE, [],
1655
    "<instance_name>", "List the tags of the given instance"),
1656
  "add-tags": (
1657
    AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1658
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1659
    "<instance_name> tag...", "Add tags to the given instance"),
1660
  "remove-tags": (
1661
    RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1662
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1663
    "<instance_name> tag...", "Remove tags from given instance"),
1664
  }
1665

    
1666
#: dictionary with aliases for commands
1667
aliases = {
1668
  "start": "startup",
1669
  "stop": "shutdown",
1670
  "show": "info",
1671
  }
1672

    
1673

    
1674
def Main():
1675
  return GenericMain(commands, aliases=aliases,
1676
                     override={"tag_type": constants.TAG_INSTANCE},
1677
                     env_override=_ENV_OVERRIDE)