Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 0c77c331

History | View | Annotate | Download (59.5 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

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

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

    
45

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

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

    
62

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

    
68

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

    
72

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

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

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

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

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

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

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

    
148
  return inames
149

    
150

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

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

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

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

    
172

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

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

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

    
206

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

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

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

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

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

    
231

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

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

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

    
245

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

249
  This is just a wrapper over GenericInstanceCreate.
250

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

    
254

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

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

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

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

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

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

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

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

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

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

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

    
339
  jex = JobExecutor(opts=opts)
340

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

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

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

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

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

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

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

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

    
406
  return 0
407

    
408

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

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

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

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

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

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

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

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

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

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

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

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

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

    
492

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

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

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

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

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

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

    
523

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

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

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

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

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

    
549
  return 0
550

    
551

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

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

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

566
  """
567
  instance_name = args[0]
568
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
569
                                       ignore_size=opts.ignore_size,
570
                                       wait_for_sync=opts.wait_for_sync)
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, errors.ECODE_INVAL)
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), errors.ECODE_INVAL)
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
    if opts.iallocator:
634
      msg = "At most one of either --nodes or --iallocator can be passed"
635
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
636
    pnode, snode = SplitNodeOption(opts.node)
637
    nodes = [pnode]
638
    if snode is not None:
639
      nodes.append(snode)
640
  else:
641
    nodes = []
642

    
643
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
644
                                       disks=disks, nodes=nodes,
645
                                       iallocator=opts.iallocator)
646
  SubmitOrSend(op, opts)
647

    
648
  return 0
649

    
650

    
651
def GrowDisk(opts, args):
652
  """Grow an instance's disks.
653

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

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

    
681

    
682
def _StartupInstance(name, opts):
683
  """Startup instances.
684

685
  This returns the opcode to start an instance, and its decorator will
686
  wrap this into a loop starting all desired instances.
687

688
  @param name: the name of the instance to act on
689
  @param opts: the command line options selected by the user
690
  @return: the opcode needed for the operation
691

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

    
705

    
706
def _RebootInstance(name, opts):
707
  """Reboot instance(s).
708

709
  This returns the opcode to reboot an instance, and its decorator
710
  will wrap this into a loop rebooting all desired instances.
711

712
  @param name: the name of the instance to act on
713
  @param opts: the command line options selected by the user
714
  @return: the opcode needed for the operation
715

716
  """
717
  return opcodes.OpInstanceReboot(instance_name=name,
718
                                  reboot_type=opts.reboot_type,
719
                                  ignore_secondaries=opts.ignore_secondaries,
720
                                  shutdown_timeout=opts.shutdown_timeout)
721

    
722

    
723
def _ShutdownInstance(name, opts):
724
  """Shutdown an instance.
725

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

729
  @param name: the name of the instance to act on
730
  @param opts: the command line options selected by the user
731
  @return: the opcode needed for the operation
732

733
  """
734
  return opcodes.OpInstanceShutdown(instance_name=name,
735
                                    timeout=opts.timeout,
736
                                    ignore_offline_nodes=opts.ignore_offline,
737
                                    no_remember=opts.no_remember)
738

    
739

    
740
def ReplaceDisks(opts, args):
741
  """Replace the disks of an instance
742

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

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

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

    
786

    
787
def FailoverInstance(opts, args):
788
  """Failover an instance.
789

790
  The failover is done by shutting it down on its present node and
791
  starting it on the secondary.
792

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

799
  """
800
  cl = GetClient()
801
  instance_name = args[0]
802
  force = opts.force
803
  iallocator = opts.iallocator
804
  target_node = opts.dst_node
805

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

    
810
  if not force:
811
    _EnsureInstancesExist(cl, [instance_name])
812

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

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

    
828

    
829
def MigrateInstance(opts, args):
830
  """Migrate an instance.
831

832
  The migrate is done without shutdown.
833

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

840
  """
841
  cl = GetClient()
842
  instance_name = args[0]
843
  force = opts.force
844
  iallocator = opts.iallocator
845
  target_node = opts.dst_node
846

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

    
851
  if not force:
852
    _EnsureInstancesExist(cl, [instance_name])
853

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

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

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

    
885

    
886
def MoveInstance(opts, args):
887
  """Move an instance.
888

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

895
  """
896
  cl = GetClient()
897
  instance_name = args[0]
898
  force = opts.force
899

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

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

    
915

    
916
def ConnectToInstanceConsole(opts, args):
917
  """Connect to the console of an instance.
918

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

925
  """
926
  instance_name = args[0]
927

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

    
937
  del cl
938

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

    
948
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
949
                    opts.show_command, cluster_name)
950

    
951

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

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

963
  """
964
  assert console.Validate()
965

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

    
983
    srun = ssh.SshRunner(cluster_name=cluster_name)
984
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
985
                            batch=True, quiet=False, tty=True)
986

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

    
1001
  return constants.EXIT_SUCCESS
1002

    
1003

    
1004
def _FormatLogicalID(dev_type, logical_id, roman):
1005
  """Formats the logical_id of a disk.
1006

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

    
1024
  return data
1025

    
1026

    
1027
def _FormatBlockDevInfo(idx, top_level, dev, roman):
1028
  """Show block device information.
1029

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

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

1044
  """
1045
  def helper(dtype, status):
1046
    """Format one line for physical device status.
1047

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

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

    
1064
    if minor is None:
1065
      minor_string = "N/A"
1066
    else:
1067
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1068

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

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

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

    
1130
  if dev["sstatus"]:
1131
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1132

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

    
1140

    
1141
def _FormatList(buf, data, indent_level):
1142
  """Formats a list of data at a given indent level.
1143

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

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

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

    
1169

    
1170
def ShowInstanceConfig(opts, args):
1171
  """Compute instance run-time status.
1172

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

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

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

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

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

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

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

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

    
1278

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

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

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

1292
  """
1293
  result = []
1294

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

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

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

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

    
1334
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1335

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

    
1340
    result.append((action, idxno, params))
1341

    
1342
  return result
1343

    
1344

    
1345
def _ParseDiskSizes(mods):
1346
  """Parses disk sizes in parameters.
1347

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

    
1357
  return mods
1358

    
1359

    
1360
def SetInstanceParams(opts, args):
1361
  """Modifies an instance.
1362

1363
  All parameters take effect only at the next restart of the instance.
1364

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

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

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

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

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

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

    
1394
  nics = _ConvertNicDiskModifications(opts.nics)
1395
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1396

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

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

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

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

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

    
1438

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

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

    
1445
  cl = GetClient()
1446

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

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

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

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

    
1469
  return rcode
1470

    
1471

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1678

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