Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ ef8270dc

History | View | Annotate | Download (59.2 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
  amount = utils.ParseUnit(args[2])
664
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
665
                                  disk=disk, amount=amount,
666
                                  wait_for_sync=opts.wait_for_sync,
667
                                  absolute=opts.absolute)
668
  SubmitOrSend(op, opts)
669
  return 0
670

    
671

    
672
def _StartupInstance(name, opts):
673
  """Startup instances.
674

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

678
  @param name: the name of the instance to act on
679
  @param opts: the command line options selected by the user
680
  @return: the opcode needed for the operation
681

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

    
695

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

699
  This returns the opcode to reboot an instance, and its decorator
700
  will wrap this into a loop rebooting 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
705

706
  """
707
  return opcodes.OpInstanceReboot(instance_name=name,
708
                                  reboot_type=opts.reboot_type,
709
                                  ignore_secondaries=opts.ignore_secondaries,
710
                                  shutdown_timeout=opts.shutdown_timeout)
711

    
712

    
713
def _ShutdownInstance(name, opts):
714
  """Shutdown an instance.
715

716
  This returns the opcode to shutdown an instance, and its decorator
717
  will wrap this into a loop shutting down all desired instances.
718

719
  @param name: the name of the instance to act on
720
  @param opts: the command line options selected by the user
721
  @return: the opcode needed for the operation
722

723
  """
724
  return opcodes.OpInstanceShutdown(instance_name=name,
725
                                    timeout=opts.timeout,
726
                                    ignore_offline_nodes=opts.ignore_offline,
727
                                    no_remember=opts.no_remember)
728

    
729

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

733
  @param opts: the command line options selected by the user
734
  @type args: list
735
  @param args: should contain only one element, the instance name
736
  @rtype: int
737
  @return: the desired exit code
738

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

    
768
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
769
                                      remote_node=new_2ndary, mode=mode,
770
                                      iallocator=iallocator,
771
                                      early_release=opts.early_release,
772
                                      ignore_ipolicy=opts.ignore_ipolicy)
773
  SubmitOrSend(op, opts)
774
  return 0
775

    
776

    
777
def FailoverInstance(opts, args):
778
  """Failover an instance.
779

780
  The failover is done by shutting it down on its present node and
781
  starting it on the secondary.
782

783
  @param opts: the command line options selected by the user
784
  @type args: list
785
  @param args: should contain only one element, the instance name
786
  @rtype: int
787
  @return: the desired exit code
788

789
  """
790
  cl = GetClient()
791
  instance_name = args[0]
792
  force = opts.force
793
  iallocator = opts.iallocator
794
  target_node = opts.dst_node
795

    
796
  if iallocator and target_node:
797
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
798
                               " node (-n) but not both", errors.ECODE_INVAL)
799

    
800
  if not force:
801
    _EnsureInstancesExist(cl, [instance_name])
802

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

    
809
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
810
                                  ignore_consistency=opts.ignore_consistency,
811
                                  shutdown_timeout=opts.shutdown_timeout,
812
                                  iallocator=iallocator,
813
                                  target_node=target_node,
814
                                  ignore_ipolicy=opts.ignore_ipolicy)
815
  SubmitOrSend(op, opts, cl=cl)
816
  return 0
817

    
818

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

822
  The migrate is done without shutdown.
823

824
  @param opts: the command line options selected by the user
825
  @type args: list
826
  @param args: should contain only one element, the instance name
827
  @rtype: int
828
  @return: the desired exit code
829

830
  """
831
  cl = GetClient()
832
  instance_name = args[0]
833
  force = opts.force
834
  iallocator = opts.iallocator
835
  target_node = opts.dst_node
836

    
837
  if iallocator and target_node:
838
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
839
                               " node (-n) but not both", errors.ECODE_INVAL)
840

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

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

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

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

    
875

    
876
def MoveInstance(opts, args):
877
  """Move an instance.
878

879
  @param opts: the command line options selected by the user
880
  @type args: list
881
  @param args: should contain only one element, the instance name
882
  @rtype: int
883
  @return: the desired exit code
884

885
  """
886
  cl = GetClient()
887
  instance_name = args[0]
888
  force = opts.force
889

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

    
897
  op = opcodes.OpInstanceMove(instance_name=instance_name,
898
                              target_node=opts.node,
899
                              shutdown_timeout=opts.shutdown_timeout,
900
                              ignore_consistency=opts.ignore_consistency,
901
                              ignore_ipolicy=opts.ignore_ipolicy)
902
  SubmitOrSend(op, opts, cl=cl)
903
  return 0
904

    
905

    
906
def ConnectToInstanceConsole(opts, args):
907
  """Connect to the console of an instance.
908

909
  @param opts: the command line options selected by the user
910
  @type args: list
911
  @param args: should contain only one element, the instance name
912
  @rtype: int
913
  @return: the desired exit code
914

915
  """
916
  instance_name = args[0]
917

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

    
927
  del cl
928

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

    
938
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
939
                    opts.show_command, cluster_name)
940

    
941

    
942
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
943
               _runcmd_fn=utils.RunCmd):
944
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
945

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

953
  """
954
  assert console.Validate()
955

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

    
973
    srun = ssh.SshRunner(cluster_name=cluster_name)
974
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
975
                            batch=True, quiet=False, tty=True)
976

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

    
991
  return constants.EXIT_SUCCESS
992

    
993

    
994
def _FormatLogicalID(dev_type, logical_id, roman):
995
  """Formats the logical_id of a disk.
996

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

    
1014
  return data
1015

    
1016

    
1017
def _FormatBlockDevInfo(idx, top_level, dev, roman):
1018
  """Show block device information.
1019

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

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

1034
  """
1035
  def helper(dtype, status):
1036
    """Format one line for physical device status.
1037

1038
    @type dtype: str
1039
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
1040
    @type status: tuple
1041
    @param status: a tuple as returned from L{backend.FindBlockDevice}
1042
    @return: the string representing the status
1043

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

    
1054
    if minor is None:
1055
      minor_string = "N/A"
1056
    else:
1057
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1058

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

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

    
1117
  if dev["pstatus"]:
1118
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1119

    
1120
  if dev["sstatus"]:
1121
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1122

    
1123
  if dev["children"]:
1124
    data.append("child devices:")
1125
    for c_idx, child in enumerate(dev["children"]):
1126
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1127
  d1.append(data)
1128
  return d1
1129

    
1130

    
1131
def _FormatList(buf, data, indent_level):
1132
  """Formats a list of data at a given indent level.
1133

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

1140
  @type buf: StringIO
1141
  @param buf: the buffer into which we write the output
1142
  @param data: the list to format
1143
  @type indent_level: int
1144
  @param indent_level: the indent level to format at
1145

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

    
1159

    
1160
def ShowInstanceConfig(opts, args):
1161
  """Compute instance run-time status.
1162

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

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

    
1180
  retcode = 0
1181
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1182
                                   use_locking=not opts.static)
1183
  result = SubmitOpCode(op, opts=opts)
1184
  if not result:
1185
    ToStdout("No instances.")
1186
    return 1
1187

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

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

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

    
1272
    for idx, device in enumerate(instance["disks"]):
1273
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1274
                  opts.roman_integers), 2)
1275

    
1276
  ToStdout(buf.getvalue().rstrip("\n"))
1277
  return retcode
1278

    
1279

    
1280
def _ConvertNicDiskModifications(mods):
1281
  """Converts NIC/disk modifications from CLI to opcode.
1282

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

1288
  @type mods: list of tuples
1289
  @param mods: Modifications as given by command line parser
1290
  @rtype: list of tuples
1291
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1292

1293
  """
1294
  result = []
1295

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

    
1313
      add = params.pop(constants.DDM_ADD, _MISSING)
1314
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1315

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

    
1326
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1327

    
1328
    if action == constants.DDM_REMOVE and params:
1329
      raise errors.OpPrereqError("Not accepting parameters on removal",
1330
                                 errors.ECODE_INVAL)
1331

    
1332
    result.append((action, idxno, params))
1333

    
1334
  return result
1335

    
1336

    
1337
def _ParseDiskSizes(mods):
1338
  """Parses disk sizes in parameters.
1339

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

    
1349
  return mods
1350

    
1351

    
1352
def SetInstanceParams(opts, args):
1353
  """Modifies an instance.
1354

1355
  All parameters take effect only at the next restart of the instance.
1356

1357
  @param opts: the command line options selected by the user
1358
  @type args: list
1359
  @param args: should contain only one element, the instance name
1360
  @rtype: int
1361
  @return: the desired exit code
1362

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

    
1370
  for param in opts.beparams:
1371
    if isinstance(opts.beparams[param], basestring):
1372
      if opts.beparams[param].lower() == "default":
1373
        opts.beparams[param] = constants.VALUE_DEFAULT
1374

    
1375
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1376
                      allowed_values=[constants.VALUE_DEFAULT])
1377

    
1378
  for param in opts.hvparams:
1379
    if isinstance(opts.hvparams[param], basestring):
1380
      if opts.hvparams[param].lower() == "default":
1381
        opts.hvparams[param] = constants.VALUE_DEFAULT
1382

    
1383
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1384
                      allowed_values=[constants.VALUE_DEFAULT])
1385

    
1386
  nics = _ConvertNicDiskModifications(opts.nics)
1387
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1388

    
1389
  if (opts.disk_template and
1390
      opts.disk_template in constants.DTS_INT_MIRROR and
1391
      not opts.node):
1392
    ToStderr("Changing the disk template to a mirrored one requires"
1393
             " specifying a secondary node")
1394
    return 1
1395

    
1396
  if opts.offline_inst:
1397
    offline = True
1398
  elif opts.online_inst:
1399
    offline = False
1400
  else:
1401
    offline = None
1402

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

    
1419
  # even if here we process the result, we allow submit only
1420
  result = SubmitOrSend(op, opts)
1421

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

    
1430

    
1431
def ChangeGroup(opts, args):
1432
  """Moves an instance to another group.
1433

1434
  """
1435
  (instance_name, ) = args
1436

    
1437
  cl = GetClient()
1438

    
1439
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1440
                                     iallocator=opts.iallocator,
1441
                                     target_groups=opts.to,
1442
                                     early_release=opts.early_release)
1443
  result = SubmitOrSend(op, opts, cl=cl)
1444

    
1445
  # Keep track of submitted jobs
1446
  jex = JobExecutor(cl=cl, opts=opts)
1447

    
1448
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1449
    jex.AddJobId(None, status, job_id)
1450

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

    
1461
  return rcode
1462

    
1463

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

    
1470
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1471
                            help="Filter by nodes (primary only)",
1472
                            const=_EXPAND_NODES_PRI, action="store_const")
1473

    
1474
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1475
                            help="Filter by nodes (secondary only)",
1476
                            const=_EXPAND_NODES_SEC, action="store_const")
1477

    
1478
m_node_opt = cli_option("--node", dest="multi_mode",
1479
                        help="Filter by nodes (primary and secondary)",
1480
                        const=_EXPAND_NODES_BOTH, action="store_const")
1481

    
1482
m_clust_opt = cli_option("--all", dest="multi_mode",
1483
                         help="Select all instances in the cluster",
1484
                         const=_EXPAND_CLUSTER, action="store_const")
1485

    
1486
m_inst_opt = cli_option("--instance", dest="multi_mode",
1487
                        help="Filter by instance name [default]",
1488
                        const=_EXPAND_INSTANCES, action="store_const")
1489

    
1490
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1491
                             help="Filter by node tag",
1492
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1493
                             action="store_const")
1494

    
1495
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1496
                                 help="Filter by primary node tag",
1497
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1498
                                 action="store_const")
1499

    
1500
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1501
                                 help="Filter by secondary node tag",
1502
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1503
                                 action="store_const")
1504

    
1505
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1506
                             help="Filter by instance tag",
1507
                             const=_EXPAND_INSTANCES_BY_TAGS,
1508
                             action="store_const")
1509

    
1510
# this is defined separately due to readability only
1511
add_opts = [
1512
  NOSTART_OPT,
1513
  OS_OPT,
1514
  FORCE_VARIANT_OPT,
1515
  NO_INSTALL_OPT,
1516
  IGNORE_IPOLICY_OPT,
1517
  ]
1518

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

    
1662
#: dictionary with aliases for commands
1663
aliases = {
1664
  "start": "startup",
1665
  "stop": "shutdown",
1666
  "show": "info",
1667
  }
1668

    
1669

    
1670
def Main():
1671
  return GenericMain(commands, aliases=aliases,
1672
                     override={"tag_type": constants.TAG_INSTANCE},
1673
                     env_override=_ENV_OVERRIDE)