Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 19ad386e

History | View | Annotate | Download (59.4 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
                               "nic.networks",
222
                               "snodes", "snodes.group", "snodes.group.uuid"],
223
                              (lambda value: ",".join(str(item)
224
                                                      for item in value),
225
                               False))
226

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

    
232

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

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

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

    
246

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

250
  This is just a wrapper over GenericInstanceCreate.
251

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

    
255

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

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

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

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

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

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

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

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

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

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

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

    
340
  jex = JobExecutor(opts=opts)
341

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

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

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

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

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

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

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

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

    
407
  return 0
408

    
409

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

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

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

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

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

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

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

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

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

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

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

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

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

    
493

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

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

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

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

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

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

    
524

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

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

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

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

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

    
550
  return 0
551

    
552

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

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

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

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

    
577

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

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

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

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

    
597

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

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

607
  """
608
  instance_name = args[0]
609

    
610
  disks = []
611

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

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

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

    
628
      disks.append((didx, ddict))
629

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

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

    
641
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
642
                                       disks=disks, nodes=nodes)
643
  SubmitOrSend(op, opts)
644

    
645
  return 0
646

    
647

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

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

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

    
678

    
679
def _StartupInstance(name, opts):
680
  """Startup instances.
681

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

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

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

    
702

    
703
def _RebootInstance(name, opts):
704
  """Reboot instance(s).
705

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

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

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

    
719

    
720
def _ShutdownInstance(name, opts):
721
  """Shutdown an instance.
722

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

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

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

    
736

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

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

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

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

    
783

    
784
def FailoverInstance(opts, args):
785
  """Failover an instance.
786

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

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

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

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

    
807
  if not force:
808
    _EnsureInstancesExist(cl, [instance_name])
809

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

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

    
825

    
826
def MigrateInstance(opts, args):
827
  """Migrate an instance.
828

829
  The migrate is done without shutdown.
830

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

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

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

    
848
  if not force:
849
    _EnsureInstancesExist(cl, [instance_name])
850

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

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

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

    
882

    
883
def MoveInstance(opts, args):
884
  """Move an instance.
885

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

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

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

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

    
912

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

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

922
  """
923
  instance_name = args[0]
924

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

    
934
  del cl
935

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

    
945
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
946
                    opts.show_command, cluster_name)
947

    
948

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

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

960
  """
961
  assert console.Validate()
962

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

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

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

    
998
  return constants.EXIT_SUCCESS
999

    
1000

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

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

    
1021
  return data
1022

    
1023

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

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

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

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

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

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

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

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

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

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

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

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

    
1137

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

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

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

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

    
1166

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

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

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

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

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

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

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

    
1269
    for idx, device in enumerate(instance["disks"]):
1270
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1271
                  opts.roman_integers), 2)
1272

    
1273
  ToStdout(buf.getvalue().rstrip("\n"))
1274
  return retcode
1275

    
1276

    
1277
def _ConvertNicDiskModifications(mods):
1278
  """Converts NIC/disk modifications from CLI to opcode.
1279

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

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

1290
  """
1291
  result = []
1292

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

    
1310
      add = params.pop(constants.DDM_ADD, _MISSING)
1311
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1312
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1313

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

    
1325
      else:
1326
        if add is _MISSING and remove is _MISSING:
1327
          action = constants.DDM_MODIFY
1328
        else:
1329
          raise errors.OpPrereqError("Cannot modify and add/remove at the"
1330
                                     " same time", errors.ECODE_INVAL)
1331

    
1332
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1333

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

    
1338
    result.append((action, idxno, params))
1339

    
1340
  return result
1341

    
1342

    
1343
def _ParseDiskSizes(mods):
1344
  """Parses disk sizes in parameters.
1345

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

    
1355
  return mods
1356

    
1357

    
1358
def SetInstanceParams(opts, args):
1359
  """Modifies an instance.
1360

1361
  All parameters take effect only at the next restart of the instance.
1362

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

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

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

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

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

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

    
1392
  nics = _ConvertNicDiskModifications(opts.nics)
1393
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1394

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

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

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

    
1426
  # even if here we process the result, we allow submit only
1427
  result = SubmitOrSend(op, opts)
1428

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

    
1437

    
1438
def ChangeGroup(opts, args):
1439
  """Moves an instance to another group.
1440

1441
  """
1442
  (instance_name, ) = args
1443

    
1444
  cl = GetClient()
1445

    
1446
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1447
                                     iallocator=opts.iallocator,
1448
                                     target_groups=opts.to,
1449
                                     early_release=opts.early_release)
1450
  result = SubmitOrSend(op, opts, cl=cl)
1451

    
1452
  # Keep track of submitted jobs
1453
  jex = JobExecutor(cl=cl, opts=opts)
1454

    
1455
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1456
    jex.AddJobId(None, status, job_id)
1457

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

    
1468
  return rcode
1469

    
1470

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

    
1477
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1478
                            help="Filter by nodes (primary only)",
1479
                            const=_EXPAND_NODES_PRI, action="store_const")
1480

    
1481
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1482
                            help="Filter by nodes (secondary only)",
1483
                            const=_EXPAND_NODES_SEC, action="store_const")
1484

    
1485
m_node_opt = cli_option("--node", dest="multi_mode",
1486
                        help="Filter by nodes (primary and secondary)",
1487
                        const=_EXPAND_NODES_BOTH, action="store_const")
1488

    
1489
m_clust_opt = cli_option("--all", dest="multi_mode",
1490
                         help="Select all instances in the cluster",
1491
                         const=_EXPAND_CLUSTER, action="store_const")
1492

    
1493
m_inst_opt = cli_option("--instance", dest="multi_mode",
1494
                        help="Filter by instance name [default]",
1495
                        const=_EXPAND_INSTANCES, action="store_const")
1496

    
1497
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1498
                             help="Filter by node tag",
1499
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1500
                             action="store_const")
1501

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

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

    
1512
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1513
                             help="Filter by instance tag",
1514
                             const=_EXPAND_INSTANCES_BY_TAGS,
1515
                             action="store_const")
1516

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

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

    
1670
#: dictionary with aliases for commands
1671
aliases = {
1672
  "start": "startup",
1673
  "stop": "shutdown",
1674
  "show": "info",
1675
  }
1676

    
1677

    
1678
def Main():
1679
  return GenericMain(commands, aliases=aliases,
1680
                     override={"tag_type": constants.TAG_INSTANCE},
1681
                     env_override=_ENV_OVERRIDE)