Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 3016bc1f

History | View | Annotate | Download (56.7 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

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

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

    
44

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

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

    
61

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

    
67

    
68
_ENV_OVERRIDE = frozenset(["list"])
69

    
70

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

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

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

93
  """
94
  # pylint: disable=W0142
95

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

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

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

    
146
  return inames
147

    
148

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

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

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

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

    
170

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

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

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

    
204

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

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

214
  """
215
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
216

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

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

    
229

    
230
def ListInstanceFields(opts, args):
231
  """List instance fields.
232

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

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

    
243

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

247
  This is just a wrapper over GenericInstanceCreate.
248

249
  """
250
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
251

    
252

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

256
  This function reads a json file with instances defined
257
  in the form::
258

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

271
  Note that I{primary_node} and I{secondary_node} have precedence over
272
  I{iallocator}.
273

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

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

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

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

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

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

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

    
337
  jex = JobExecutor(opts=opts)
338

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

    
348
    hypervisor = specs["hypervisor"]
349
    hvparams = specs["hvparams"]
350

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

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

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

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

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

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

    
404
  return 0
405

    
406

    
407
def ReinstallInstance(opts, args):
408
  """Reinstall an instance.
409

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

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

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

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

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

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

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

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

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

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

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

    
487
  jex.WaitOrShow(not opts.submit_only)
488
  return 0
489

    
490

    
491
def RemoveInstance(opts, args):
492
  """Remove an instance.
493

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

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

    
506
  if not force:
507
    _EnsureInstancesExist(cl, [instance_name])
508

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

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

    
521

    
522
def RenameInstance(opts, args):
523
  """Rename an instance.
524

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

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

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

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

    
547
  return 0
548

    
549

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

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

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

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

    
573

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

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

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

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

    
593

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

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

603
  """
604
  instance_name = args[0]
605

    
606
  disks = []
607

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

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

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

    
624
      disks.append((didx, ddict))
625

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

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

    
637
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
638
                                       disks=disks, nodes=nodes)
639
  SubmitOrSend(op, opts)
640

    
641
  return 0
642

    
643

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

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

654
  """
655
  instance = args[0]
656
  disk = args[1]
657
  try:
658
    disk = int(disk)
659
  except (TypeError, ValueError), err:
660
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
661
                               errors.ECODE_INVAL)
662
  amount = utils.ParseUnit(args[2])
663
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
664
                                  disk=disk, amount=amount,
665
                                  wait_for_sync=opts.wait_for_sync)
666
  SubmitOrSend(op, opts)
667
  return 0
668

    
669

    
670
def _StartupInstance(name, opts):
671
  """Startup instances.
672

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

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

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

    
693

    
694
def _RebootInstance(name, opts):
695
  """Reboot instance(s).
696

697
  This returns the opcode to reboot an instance, and its decorator
698
  will wrap this into a loop rebooting all desired instances.
699

700
  @param name: the name of the instance to act on
701
  @param opts: the command line options selected by the user
702
  @return: the opcode needed for the operation
703

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

    
710

    
711
def _ShutdownInstance(name, opts):
712
  """Shutdown an instance.
713

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

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

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

    
727

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

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

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

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

    
774

    
775
def FailoverInstance(opts, args):
776
  """Failover an instance.
777

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

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

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

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

    
798
  if not force:
799
    _EnsureInstancesExist(cl, [instance_name])
800

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

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

    
816

    
817
def MigrateInstance(opts, args):
818
  """Migrate an instance.
819

820
  The migrate is done without shutdown.
821

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

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

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

    
839
  if not force:
840
    _EnsureInstancesExist(cl, [instance_name])
841

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

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

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

    
873

    
874
def MoveInstance(opts, args):
875
  """Move an instance.
876

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

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

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

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

    
903

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

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

913
  """
914
  instance_name = args[0]
915

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

    
925
  del cl
926

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

    
936
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
937
                    opts.show_command, cluster_name)
938

    
939

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

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

951
  """
952
  assert console.Validate()
953

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

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

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

    
989
  return constants.EXIT_SUCCESS
990

    
991

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

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

    
1012
  return data
1013

    
1014

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

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

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

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

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

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

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

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

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

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

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

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

    
1128

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

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

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

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

    
1157

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

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

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

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

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

    
1215
    # custom VNC console information
1216
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1217
                                                 None)
1218
    if vnc_bind_address:
1219
      port = instance["network_port"]
1220
      display = int(port) - constants.VNC_BASE_PORT
1221
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1222
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1223
                                                   port,
1224
                                                   display)
1225
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1226
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1227
                             (vnc_bind_address, port,
1228
                              instance["pnode"], display))
1229
      else:
1230
        # vnc bind address is a file
1231
        vnc_console_port = "%s:%s" % (instance["pnode"],
1232
                                      vnc_bind_address)
1233
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1234

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

    
1262
    for idx, device in enumerate(instance["disks"]):
1263
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1264
                  opts.roman_integers), 2)
1265

    
1266
  ToStdout(buf.getvalue().rstrip("\n"))
1267
  return retcode
1268

    
1269

    
1270
def SetInstanceParams(opts, args):
1271
  """Modifies an instance.
1272

1273
  All parameters take effect only at the next restart of the instance.
1274

1275
  @param opts: the command line options selected by the user
1276
  @type args: list
1277
  @param args: should contain only one element, the instance name
1278
  @rtype: int
1279
  @return: the desired exit code
1280

1281
  """
1282
  if not (opts.nics or opts.disks or opts.disk_template or
1283
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1284
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1285
    ToStderr("Please give at least one of the parameters.")
1286
    return 1
1287

    
1288
  for param in opts.beparams:
1289
    if isinstance(opts.beparams[param], basestring):
1290
      if opts.beparams[param].lower() == "default":
1291
        opts.beparams[param] = constants.VALUE_DEFAULT
1292

    
1293
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1294
                      allowed_values=[constants.VALUE_DEFAULT])
1295

    
1296
  for param in opts.hvparams:
1297
    if isinstance(opts.hvparams[param], basestring):
1298
      if opts.hvparams[param].lower() == "default":
1299
        opts.hvparams[param] = constants.VALUE_DEFAULT
1300

    
1301
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1302
                      allowed_values=[constants.VALUE_DEFAULT])
1303

    
1304
  for idx, (nic_op, nic_dict) in enumerate(opts.nics):
1305
    try:
1306
      nic_op = int(nic_op)
1307
      opts.nics[idx] = (nic_op, nic_dict)
1308
    except (TypeError, ValueError):
1309
      pass
1310

    
1311
  for idx, (disk_op, disk_dict) in enumerate(opts.disks):
1312
    try:
1313
      disk_op = int(disk_op)
1314
      opts.disks[idx] = (disk_op, disk_dict)
1315
    except (TypeError, ValueError):
1316
      pass
1317
    if disk_op == constants.DDM_ADD:
1318
      if "size" not in disk_dict:
1319
        raise errors.OpPrereqError("Missing required parameter 'size'",
1320
                                   errors.ECODE_INVAL)
1321
      disk_dict["size"] = utils.ParseUnit(disk_dict["size"])
1322

    
1323
  if (opts.disk_template and
1324
      opts.disk_template in constants.DTS_INT_MIRROR and
1325
      not opts.node):
1326
    ToStderr("Changing the disk template to a mirrored one requires"
1327
             " specifying a secondary node")
1328
    return 1
1329

    
1330
  if opts.offline_inst:
1331
    offline = True
1332
  elif opts.online_inst:
1333
    offline = False
1334
  else:
1335
    offline = None
1336

    
1337
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1338
                                   nics=opts.nics,
1339
                                   disks=opts.disks,
1340
                                   disk_template=opts.disk_template,
1341
                                   remote_node=opts.node,
1342
                                   hvparams=opts.hvparams,
1343
                                   beparams=opts.beparams,
1344
                                   runtime_mem=opts.runtime_mem,
1345
                                   os_name=opts.os,
1346
                                   osparams=opts.osparams,
1347
                                   force_variant=opts.force_variant,
1348
                                   force=opts.force,
1349
                                   wait_for_sync=opts.wait_for_sync,
1350
                                   offline=offline,
1351
                                   ignore_ipolicy=opts.ignore_ipolicy)
1352

    
1353
  # even if here we process the result, we allow submit only
1354
  result = SubmitOrSend(op, opts)
1355

    
1356
  if result:
1357
    ToStdout("Modified instance %s", args[0])
1358
    for param, data in result:
1359
      ToStdout(" - %-5s -> %s", param, data)
1360
    ToStdout("Please don't forget that most parameters take effect"
1361
             " only at the next start of the instance.")
1362
  return 0
1363

    
1364

    
1365
def ChangeGroup(opts, args):
1366
  """Moves an instance to another group.
1367

1368
  """
1369
  (instance_name, ) = args
1370

    
1371
  cl = GetClient()
1372

    
1373
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1374
                                     iallocator=opts.iallocator,
1375
                                     target_groups=opts.to,
1376
                                     early_release=opts.early_release)
1377
  result = SubmitOpCode(op, cl=cl, opts=opts)
1378

    
1379
  # Keep track of submitted jobs
1380
  jex = JobExecutor(cl=cl, opts=opts)
1381

    
1382
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1383
    jex.AddJobId(None, status, job_id)
1384

    
1385
  results = jex.GetResults()
1386
  bad_cnt = len([row for row in results if not row[0]])
1387
  if bad_cnt == 0:
1388
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1389
    rcode = constants.EXIT_SUCCESS
1390
  else:
1391
    ToStdout("There were %s errors while changing group of instance '%s'.",
1392
             bad_cnt, instance_name)
1393
    rcode = constants.EXIT_FAILURE
1394

    
1395
  return rcode
1396

    
1397

    
1398
# multi-instance selection options
1399
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1400
                           help="Do not ask for confirmation when more than"
1401
                           " one instance is affected",
1402
                           action="store_true", default=False)
1403

    
1404
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1405
                            help="Filter by nodes (primary only)",
1406
                            const=_EXPAND_NODES_PRI, action="store_const")
1407

    
1408
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1409
                            help="Filter by nodes (secondary only)",
1410
                            const=_EXPAND_NODES_SEC, action="store_const")
1411

    
1412
m_node_opt = cli_option("--node", dest="multi_mode",
1413
                        help="Filter by nodes (primary and secondary)",
1414
                        const=_EXPAND_NODES_BOTH, action="store_const")
1415

    
1416
m_clust_opt = cli_option("--all", dest="multi_mode",
1417
                         help="Select all instances in the cluster",
1418
                         const=_EXPAND_CLUSTER, action="store_const")
1419

    
1420
m_inst_opt = cli_option("--instance", dest="multi_mode",
1421
                        help="Filter by instance name [default]",
1422
                        const=_EXPAND_INSTANCES, action="store_const")
1423

    
1424
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1425
                             help="Filter by node tag",
1426
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1427
                             action="store_const")
1428

    
1429
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1430
                                 help="Filter by primary node tag",
1431
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1432
                                 action="store_const")
1433

    
1434
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1435
                                 help="Filter by secondary node tag",
1436
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1437
                                 action="store_const")
1438

    
1439
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1440
                             help="Filter by instance tag",
1441
                             const=_EXPAND_INSTANCES_BY_TAGS,
1442
                             action="store_const")
1443

    
1444
# this is defined separately due to readability only
1445
add_opts = [
1446
  NOSTART_OPT,
1447
  OS_OPT,
1448
  FORCE_VARIANT_OPT,
1449
  NO_INSTALL_OPT,
1450
  IGNORE_IPOLICY_OPT,
1451
  ]
1452

    
1453
commands = {
1454
  "add": (
1455
    AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
1456
    "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
1457
    "Creates and adds a new instance to the cluster"),
1458
  "batch-create": (
1459
    BatchCreate, [ArgFile(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT],
1460
    "<instances.json>",
1461
    "Create a bunch of instances based on specs in the file."),
1462
  "console": (
1463
    ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
1464
    [SHOWCMD_OPT, PRIORITY_OPT],
1465
    "[--show-cmd] <instance>", "Opens a console on the specified instance"),
1466
  "failover": (
1467
    FailoverInstance, ARGS_ONE_INSTANCE,
1468
    [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
1469
     DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
1470
     IGNORE_IPOLICY_OPT],
1471
    "[-f] <instance>", "Stops the instance, changes its primary node and"
1472
    " (if it was originally running) starts it on the new node"
1473
    " (the secondary for mirrored instances or any node"
1474
    " for shared storage)."),
1475
  "migrate": (
1476
    MigrateInstance, ARGS_ONE_INSTANCE,
1477
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
1478
     PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
1479
     IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT],
1480
    "[-f] <instance>", "Migrate instance to its secondary node"
1481
    " (only for mirrored instances)"),
1482
  "move": (
1483
    MoveInstance, ARGS_ONE_INSTANCE,
1484
    [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
1485
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT, IGNORE_IPOLICY_OPT],
1486
    "[-f] <instance>", "Move instance to an arbitrary node"
1487
    " (only for instances of type file and lv)"),
1488
  "info": (
1489
    ShowInstanceConfig, ARGS_MANY_INSTANCES,
1490
    [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
1491
    "[-s] {--all | <instance>...}",
1492
    "Show information on the specified instance(s)"),
1493
  "list": (
1494
    ListInstances, ARGS_MANY_INSTANCES,
1495
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1496
     FORCE_FILTER_OPT],
1497
    "[<instance>...]",
1498
    "Lists the instances and their status. The available fields can be shown"
1499
    " using the \"list-fields\" command (see the man page for details)."
1500
    " The default field list is (in order): %s." %
1501
    utils.CommaJoin(_LIST_DEF_FIELDS),
1502
    ),
1503
  "list-fields": (
1504
    ListInstanceFields, [ArgUnknown()],
1505
    [NOHDR_OPT, SEP_OPT],
1506
    "[fields...]",
1507
    "Lists all available fields for instances"),
1508
  "reinstall": (
1509
    ReinstallInstance, [ArgInstance()],
1510
    [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
1511
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
1512
     m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1513
     SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1514
    "[-f] <instance>", "Reinstall a stopped instance"),
1515
  "remove": (
1516
    RemoveInstance, ARGS_ONE_INSTANCE,
1517
    [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
1518
     DRY_RUN_OPT, PRIORITY_OPT],
1519
    "[-f] <instance>", "Shuts down the instance and removes it"),
1520
  "rename": (
1521
    RenameInstance,
1522
    [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
1523
    [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1524
    "<instance> <new_name>", "Rename the instance"),
1525
  "replace-disks": (
1526
    ReplaceDisks, ARGS_ONE_INSTANCE,
1527
    [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
1528
     NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
1529
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
1530
    "[-s|-p|-n NODE|-I NAME] <instance>",
1531
    "Replaces all disks for the instance"),
1532
  "modify": (
1533
    SetInstanceParams, ARGS_ONE_INSTANCE,
1534
    [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
1535
     DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
1536
     OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
1537
     ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT],
1538
    "<instance>", "Alters the parameters of an instance"),
1539
  "shutdown": (
1540
    GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
1541
    [m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
1542
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1543
     m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
1544
     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],
1545
    "<instance>", "Stops an instance"),
1546
  "startup": (
1547
    GenericManyOps("startup", _StartupInstance), [ArgInstance()],
1548
    [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
1549
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1550
     m_inst_tags_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, HVOPTS_OPT,
1551
     BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT,
1552
     NO_REMEMBER_OPT, STARTUP_PAUSED_OPT],
1553
    "<instance>", "Starts an instance"),
1554
  "reboot": (
1555
    GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
1556
    [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
1557
     m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
1558
     m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
1559
     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1560
    "<instance>", "Reboots an instance"),
1561
  "activate-disks": (
1562
    ActivateDisks, ARGS_ONE_INSTANCE,
1563
    [SUBMIT_OPT, IGNORE_SIZE_OPT, PRIORITY_OPT],
1564
    "<instance>", "Activate an instance's disks"),
1565
  "deactivate-disks": (
1566
    DeactivateDisks, ARGS_ONE_INSTANCE,
1567
    [FORCE_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1568
    "[-f] <instance>", "Deactivate an instance's disks"),
1569
  "recreate-disks": (
1570
    RecreateDisks, ARGS_ONE_INSTANCE,
1571
    [SUBMIT_OPT, DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1572
    "<instance>", "Recreate an instance's disks"),
1573
  "grow-disk": (
1574
    GrowDisk,
1575
    [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
1576
     ArgUnknown(min=1, max=1)],
1577
    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1578
    "<instance> <disk> <size>", "Grow an instance's disk"),
1579
  "change-group": (
1580
    ChangeGroup, ARGS_ONE_INSTANCE,
1581
    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT],
1582
    "[-I <iallocator>] [--to <group>]", "Change group of instance"),
1583
  "list-tags": (
1584
    ListTags, ARGS_ONE_INSTANCE, [PRIORITY_OPT],
1585
    "<instance_name>", "List the tags of the given instance"),
1586
  "add-tags": (
1587
    AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1588
    [TAG_SRC_OPT, PRIORITY_OPT],
1589
    "<instance_name> tag...", "Add tags to the given instance"),
1590
  "remove-tags": (
1591
    RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
1592
    [TAG_SRC_OPT, PRIORITY_OPT],
1593
    "<instance_name> tag...", "Remove tags from given instance"),
1594
  }
1595

    
1596
#: dictionary with aliases for commands
1597
aliases = {
1598
  "start": "startup",
1599
  "stop": "shutdown",
1600
  }
1601

    
1602

    
1603
def Main():
1604
  return GenericMain(commands, aliases=aliases,
1605
                     override={"tag_type": constants.TAG_INSTANCE},
1606
                     env_override=_ENV_OVERRIDE)