Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 8c4771b1

History | View | Annotate | Download (59.3 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

    
28
import 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
  disks_info = SubmitOrSend(op, opts)
572
  for host, iname, nname in disks_info:
573
    ToStdout("%s:%s:%s", host, iname, nname)
574
  return 0
575

    
576

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

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

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

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

    
596

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

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

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

    
609
  disks = []
610

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

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

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

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

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

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

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

    
644
  return 0
645

    
646

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

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

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

    
677

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

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

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

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

    
701

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

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

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

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

    
718

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

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

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

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

    
735

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

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

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

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

    
782

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

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

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

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

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

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

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

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

    
824

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

828
  The migrate is done without shutdown.
829

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

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

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

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

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

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

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

    
881

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

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

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

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

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

    
911

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

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

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

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

    
933
  del cl
934

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

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

    
947

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

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

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

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

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

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

    
997
  return constants.EXIT_SUCCESS
998

    
999

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

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

    
1020
  return data
1021

    
1022

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

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

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

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

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

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

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

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

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

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

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

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

    
1136

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

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

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

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

    
1165

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

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

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

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

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

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

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

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

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

    
1275

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

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

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

1289
  """
1290
  result = []
1291

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

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

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

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

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

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

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

    
1339
  return result
1340

    
1341

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

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

    
1354
  return mods
1355

    
1356

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1436

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

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

    
1443
  cl = GetClient()
1444

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

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

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

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

    
1467
  return rcode
1468

    
1469

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1676

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