Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 83d4ba5e

History | View | Annotate | Download (58.8 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 copy
29
import itertools
30
import simplejson
31
import logging
32
from cStringIO import StringIO
33

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

    
45

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

    
56
_EXPAND_NODES_TAGS_MODES = frozenset([
57
  _EXPAND_NODES_BOTH_BY_TAGS,
58
  _EXPAND_NODES_PRI_BY_TAGS,
59
  _EXPAND_NODES_SEC_BY_TAGS,
60
  ])
61

    
62

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

    
68

    
69
_MISSING = object()
70
_ENV_OVERRIDE = frozenset(["list"])
71

    
72

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

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

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

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

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

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

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

    
148
  return inames
149

    
150

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

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

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

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

    
172

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

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

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

    
206

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

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

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

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

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

    
231

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

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

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

    
245

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

249
  This is just a wrapper over GenericInstanceCreate.
250

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

    
254

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

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

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

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

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

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

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

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

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

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

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

    
339
  jex = JobExecutor(opts=opts)
340

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

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

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

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

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

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

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

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

    
406
  return 0
407

    
408

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

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

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

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

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

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

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

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

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

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

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

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

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

    
492

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

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

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

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

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

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

    
523

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

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

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

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

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

    
549
  return 0
550

    
551

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

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

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

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

    
575

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

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

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

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

    
595

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

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

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

    
608
  disks = []
609

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

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

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

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

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

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

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

    
643
  return 0
644

    
645

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

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

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

    
676

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

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

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

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

    
700

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

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

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

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

    
717

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

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

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

728
  """
729
  return opcodes.OpInstanceShutdown(instance_name=name,
730
                                    timeout=opts.timeout,
731
                                    ignore_offline_nodes=opts.ignore_offline,
732
                                    no_remember=opts.no_remember)
733

    
734

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

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

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

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

    
781

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

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

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

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

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

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

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

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

    
823

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

827
  The migrate is done without shutdown.
828

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

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

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

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

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

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

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

    
880

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

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

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

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

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

    
910

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

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

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

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

    
932
  del cl
933

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

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

    
946

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

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

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

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

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

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

    
996
  return constants.EXIT_SUCCESS
997

    
998

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

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

    
1019
  return data
1020

    
1021

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

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

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

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

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

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

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

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

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

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

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

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

    
1135

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

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

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

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

    
1164

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

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

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

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

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

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

    
1250
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1251
                        level=2)
1252
    buf.write("  Hardware:\n")
1253
    # deprecated "memory" value, kept for one version for compatibility
1254
    # TODO(ganeti 2.7) remove.
1255
    be_actual = copy.deepcopy(instance["be_actual"])
1256
    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1257
    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
1258
    # TODO(ganeti 2.7) rework the NICs as well
1259
    buf.write("    - NICs:\n")
1260
    for idx, (ip, mac, mode, link) in enumerate(instance["nics"]):
1261
      buf.write("      - nic/%d: MAC: %s, IP: %s, mode: %s, link: %s\n" %
1262
                (idx, mac, ip, mode, link))
1263
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1264
    buf.write("  Disks:\n")
1265

    
1266
    for idx, device in enumerate(instance["disks"]):
1267
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1268
                  opts.roman_integers), 2)
1269

    
1270
  ToStdout(buf.getvalue().rstrip("\n"))
1271
  return retcode
1272

    
1273

    
1274
def _ConvertNicDiskModifications(mods):
1275
  """Converts NIC/disk modifications from CLI to opcode.
1276

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

1282
  @type mods: list of tuples
1283
  @param mods: Modifications as given by command line parser
1284
  @rtype: list of tuples
1285
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1286

1287
  """
1288
  result = []
1289

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

    
1307
      add = params.pop(constants.DDM_ADD, _MISSING)
1308
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1309

    
1310
      if not (add is _MISSING or remove is _MISSING):
1311
        raise errors.OpPrereqError("Cannot add and remove at the same time",
1312
                                   errors.ECODE_INVAL)
1313
      elif add is not _MISSING:
1314
        action = constants.DDM_ADD
1315
      elif remove is not _MISSING:
1316
        action = constants.DDM_REMOVE
1317
      else:
1318
        action = constants.DDM_MODIFY
1319

    
1320
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1321

    
1322
    if action == constants.DDM_REMOVE and params:
1323
      raise errors.OpPrereqError("Not accepting parameters on removal",
1324
                                 errors.ECODE_INVAL)
1325

    
1326
    result.append((action, idxno, params))
1327

    
1328
  return result
1329

    
1330

    
1331
def _ParseDiskSizes(mods):
1332
  """Parses disk sizes in parameters.
1333

1334
  """
1335
  for (action, _, params) in mods:
1336
    if params and constants.IDISK_SIZE in params:
1337
      params[constants.IDISK_SIZE] = \
1338
        utils.ParseUnit(params[constants.IDISK_SIZE])
1339
    elif action == constants.DDM_ADD:
1340
      raise errors.OpPrereqError("Missing required parameter 'size'",
1341
                                 errors.ECODE_INVAL)
1342

    
1343
  return mods
1344

    
1345

    
1346
def SetInstanceParams(opts, args):
1347
  """Modifies an instance.
1348

1349
  All parameters take effect only at the next restart of the instance.
1350

1351
  @param opts: the command line options selected by the user
1352
  @type args: list
1353
  @param args: should contain only one element, the instance name
1354
  @rtype: int
1355
  @return: the desired exit code
1356

1357
  """
1358
  if not (opts.nics or opts.disks or opts.disk_template or
1359
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1360
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1361
    ToStderr("Please give at least one of the parameters.")
1362
    return 1
1363

    
1364
  for param in opts.beparams:
1365
    if isinstance(opts.beparams[param], basestring):
1366
      if opts.beparams[param].lower() == "default":
1367
        opts.beparams[param] = constants.VALUE_DEFAULT
1368

    
1369
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1370
                      allowed_values=[constants.VALUE_DEFAULT])
1371

    
1372
  for param in opts.hvparams:
1373
    if isinstance(opts.hvparams[param], basestring):
1374
      if opts.hvparams[param].lower() == "default":
1375
        opts.hvparams[param] = constants.VALUE_DEFAULT
1376

    
1377
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1378
                      allowed_values=[constants.VALUE_DEFAULT])
1379

    
1380
  nics = _ConvertNicDiskModifications(opts.nics)
1381
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1382

    
1383
  if (opts.disk_template and
1384
      opts.disk_template in constants.DTS_INT_MIRROR and
1385
      not opts.node):
1386
    ToStderr("Changing the disk template to a mirrored one requires"
1387
             " specifying a secondary node")
1388
    return 1
1389

    
1390
  if opts.offline_inst:
1391
    offline = True
1392
  elif opts.online_inst:
1393
    offline = False
1394
  else:
1395
    offline = None
1396

    
1397
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1398
                                   nics=nics,
1399
                                   disks=disks,
1400
                                   disk_template=opts.disk_template,
1401
                                   remote_node=opts.node,
1402
                                   hvparams=opts.hvparams,
1403
                                   beparams=opts.beparams,
1404
                                   runtime_mem=opts.runtime_mem,
1405
                                   os_name=opts.os,
1406
                                   osparams=opts.osparams,
1407
                                   force_variant=opts.force_variant,
1408
                                   force=opts.force,
1409
                                   wait_for_sync=opts.wait_for_sync,
1410
                                   offline=offline,
1411
                                   ignore_ipolicy=opts.ignore_ipolicy)
1412

    
1413
  # even if here we process the result, we allow submit only
1414
  result = SubmitOrSend(op, opts)
1415

    
1416
  if result:
1417
    ToStdout("Modified instance %s", args[0])
1418
    for param, data in result:
1419
      ToStdout(" - %-5s -> %s", param, data)
1420
    ToStdout("Please don't forget that most parameters take effect"
1421
             " only at the next start of the instance.")
1422
  return 0
1423

    
1424

    
1425
def ChangeGroup(opts, args):
1426
  """Moves an instance to another group.
1427

1428
  """
1429
  (instance_name, ) = args
1430

    
1431
  cl = GetClient()
1432

    
1433
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1434
                                     iallocator=opts.iallocator,
1435
                                     target_groups=opts.to,
1436
                                     early_release=opts.early_release)
1437
  result = SubmitOrSend(op, opts, cl=cl)
1438

    
1439
  # Keep track of submitted jobs
1440
  jex = JobExecutor(cl=cl, opts=opts)
1441

    
1442
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1443
    jex.AddJobId(None, status, job_id)
1444

    
1445
  results = jex.GetResults()
1446
  bad_cnt = len([row for row in results if not row[0]])
1447
  if bad_cnt == 0:
1448
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1449
    rcode = constants.EXIT_SUCCESS
1450
  else:
1451
    ToStdout("There were %s errors while changing group of instance '%s'.",
1452
             bad_cnt, instance_name)
1453
    rcode = constants.EXIT_FAILURE
1454

    
1455
  return rcode
1456

    
1457

    
1458
# multi-instance selection options
1459
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1460
                           help="Do not ask for confirmation when more than"
1461
                           " one instance is affected",
1462
                           action="store_true", default=False)
1463

    
1464
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1465
                            help="Filter by nodes (primary only)",
1466
                            const=_EXPAND_NODES_PRI, action="store_const")
1467

    
1468
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1469
                            help="Filter by nodes (secondary only)",
1470
                            const=_EXPAND_NODES_SEC, action="store_const")
1471

    
1472
m_node_opt = cli_option("--node", dest="multi_mode",
1473
                        help="Filter by nodes (primary and secondary)",
1474
                        const=_EXPAND_NODES_BOTH, action="store_const")
1475

    
1476
m_clust_opt = cli_option("--all", dest="multi_mode",
1477
                         help="Select all instances in the cluster",
1478
                         const=_EXPAND_CLUSTER, action="store_const")
1479

    
1480
m_inst_opt = cli_option("--instance", dest="multi_mode",
1481
                        help="Filter by instance name [default]",
1482
                        const=_EXPAND_INSTANCES, action="store_const")
1483

    
1484
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1485
                             help="Filter by node tag",
1486
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1487
                             action="store_const")
1488

    
1489
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1490
                                 help="Filter by primary node tag",
1491
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1492
                                 action="store_const")
1493

    
1494
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1495
                                 help="Filter by secondary node tag",
1496
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1497
                                 action="store_const")
1498

    
1499
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1500
                             help="Filter by instance tag",
1501
                             const=_EXPAND_INSTANCES_BY_TAGS,
1502
                             action="store_const")
1503

    
1504
# this is defined separately due to readability only
1505
add_opts = [
1506
  NOSTART_OPT,
1507
  OS_OPT,
1508
  FORCE_VARIANT_OPT,
1509
  NO_INSTALL_OPT,
1510
  IGNORE_IPOLICY_OPT,
1511
  ]
1512

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

    
1656
#: dictionary with aliases for commands
1657
aliases = {
1658
  "start": "startup",
1659
  "stop": "shutdown",
1660
  "show": "info",
1661
  }
1662

    
1663

    
1664
def Main():
1665
  return GenericMain(commands, aliases=aliases,
1666
                     override={"tag_type": constants.TAG_INSTANCE},
1667
                     env_override=_ENV_OVERRIDE)