Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 2c0af7da

History | View | Annotate | Download (56.1 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

    
43

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

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

    
60

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

    
66

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

    
69

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

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

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

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

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

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

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

    
145
  return inames
146

    
147

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

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

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

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

    
169

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

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

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

    
203

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

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

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

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

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

    
228

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

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

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

    
242

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

246
  This is just a wrapper over GenericInstanceCreate.
247

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

    
251

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

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

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

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

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

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

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

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

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

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

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

    
336
  jex = JobExecutor(opts=opts)
337

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

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

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

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

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

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

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

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

    
403
  return 0
404

    
405

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

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

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

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

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

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

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

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

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

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

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

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

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

    
489

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

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

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

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

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

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

    
520

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

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

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

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

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

    
546
  return 0
547

    
548

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

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

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

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

    
572

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

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

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

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

    
592

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

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

602
  """
603
  instance_name = args[0]
604
  if opts.disks:
605
    try:
606
      opts.disks = [int(v) for v in opts.disks.split(",")]
607
    except (ValueError, TypeError), err:
608
      ToStderr("Invalid disks value: %s" % str(err))
609
      return 1
610
  else:
611
    opts.disks = []
612

    
613
  if opts.node:
614
    pnode, snode = SplitNodeOption(opts.node)
615
    nodes = [pnode]
616
    if snode is not None:
617
      nodes.append(snode)
618
  else:
619
    nodes = []
620

    
621
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
622
                                       disks=opts.disks,
623
                                       nodes=nodes)
624
  SubmitOrSend(op, opts)
625
  return 0
626

    
627

    
628
def GrowDisk(opts, args):
629
  """Grow an instance's disks.
630

631
  @param opts: the command line options selected by the user
632
  @type args: list
633
  @param args: should contain three elements, the target instance name,
634
      the target disk id, and the target growth
635
  @rtype: int
636
  @return: the desired exit code
637

638
  """
639
  instance = args[0]
640
  disk = args[1]
641
  try:
642
    disk = int(disk)
643
  except (TypeError, ValueError), err:
644
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
645
                               errors.ECODE_INVAL)
646
  amount = utils.ParseUnit(args[2])
647
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
648
                                  disk=disk, amount=amount,
649
                                  wait_for_sync=opts.wait_for_sync)
650
  SubmitOrSend(op, opts)
651
  return 0
652

    
653

    
654
def _StartupInstance(name, opts):
655
  """Startup instances.
656

657
  This returns the opcode to start an instance, and its decorator will
658
  wrap this into a loop starting all desired instances.
659

660
  @param name: the name of the instance to act on
661
  @param opts: the command line options selected by the user
662
  @return: the opcode needed for the operation
663

664
  """
665
  op = opcodes.OpInstanceStartup(instance_name=name,
666
                                 force=opts.force,
667
                                 ignore_offline_nodes=opts.ignore_offline,
668
                                 no_remember=opts.no_remember,
669
                                 startup_paused=opts.startup_paused)
670
  # do not add these parameters to the opcode unless they're defined
671
  if opts.hvparams:
672
    op.hvparams = opts.hvparams
673
  if opts.beparams:
674
    op.beparams = opts.beparams
675
  return op
676

    
677

    
678
def _RebootInstance(name, opts):
679
  """Reboot instance(s).
680

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

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

688
  """
689
  return opcodes.OpInstanceReboot(instance_name=name,
690
                                  reboot_type=opts.reboot_type,
691
                                  ignore_secondaries=opts.ignore_secondaries,
692
                                  shutdown_timeout=opts.shutdown_timeout)
693

    
694

    
695
def _ShutdownInstance(name, opts):
696
  """Shutdown an instance.
697

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

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

705
  """
706
  return opcodes.OpInstanceShutdown(instance_name=name,
707
                                    timeout=opts.timeout,
708
                                    ignore_offline_nodes=opts.ignore_offline,
709
                                    no_remember=opts.no_remember)
710

    
711

    
712
def ReplaceDisks(opts, args):
713
  """Replace the disks of an instance
714

715
  @param opts: the command line options selected by the user
716
  @type args: list
717
  @param args: should contain only one element, the instance name
718
  @rtype: int
719
  @return: the desired exit code
720

721
  """
722
  new_2ndary = opts.dst_node
723
  iallocator = opts.iallocator
724
  if opts.disks is None:
725
    disks = []
726
  else:
727
    try:
728
      disks = [int(i) for i in opts.disks.split(",")]
729
    except (TypeError, ValueError), err:
730
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
731
                                 errors.ECODE_INVAL)
732
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
733
         new_2ndary is not None, iallocator is not None].count(True)
734
  if cnt != 1:
735
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
736
                               " options must be passed", errors.ECODE_INVAL)
737
  elif opts.on_primary:
738
    mode = constants.REPLACE_DISK_PRI
739
  elif opts.on_secondary:
740
    mode = constants.REPLACE_DISK_SEC
741
  elif opts.auto:
742
    mode = constants.REPLACE_DISK_AUTO
743
    if disks:
744
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
745
                                 " mode", errors.ECODE_INVAL)
746
  elif new_2ndary is not None or iallocator is not None:
747
    # replace secondary
748
    mode = constants.REPLACE_DISK_CHG
749

    
750
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
751
                                      remote_node=new_2ndary, mode=mode,
752
                                      iallocator=iallocator,
753
                                      early_release=opts.early_release,
754
                                      ignore_ipolicy=opts.ignore_ipolicy)
755
  SubmitOrSend(op, opts)
756
  return 0
757

    
758

    
759
def FailoverInstance(opts, args):
760
  """Failover an instance.
761

762
  The failover is done by shutting it down on its present node and
763
  starting it on the secondary.
764

765
  @param opts: the command line options selected by the user
766
  @type args: list
767
  @param args: should contain only one element, the instance name
768
  @rtype: int
769
  @return: the desired exit code
770

771
  """
772
  cl = GetClient()
773
  instance_name = args[0]
774
  force = opts.force
775
  iallocator = opts.iallocator
776
  target_node = opts.dst_node
777

    
778
  if iallocator and target_node:
779
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
780
                               " node (-n) but not both", errors.ECODE_INVAL)
781

    
782
  if not force:
783
    _EnsureInstancesExist(cl, [instance_name])
784

    
785
    usertext = ("Failover will happen to image %s."
786
                " This requires a shutdown of the instance. Continue?" %
787
                (instance_name,))
788
    if not AskUser(usertext):
789
      return 1
790

    
791
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
792
                                  ignore_consistency=opts.ignore_consistency,
793
                                  shutdown_timeout=opts.shutdown_timeout,
794
                                  iallocator=iallocator,
795
                                  target_node=target_node,
796
                                  ignore_ipolicy=opts.ignore_ipolicy)
797
  SubmitOrSend(op, opts, cl=cl)
798
  return 0
799

    
800

    
801
def MigrateInstance(opts, args):
802
  """Migrate an instance.
803

804
  The migrate is done without shutdown.
805

806
  @param opts: the command line options selected by the user
807
  @type args: list
808
  @param args: should contain only one element, the instance name
809
  @rtype: int
810
  @return: the desired exit code
811

812
  """
813
  cl = GetClient()
814
  instance_name = args[0]
815
  force = opts.force
816
  iallocator = opts.iallocator
817
  target_node = opts.dst_node
818

    
819
  if iallocator and target_node:
820
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
821
                               " node (-n) but not both", errors.ECODE_INVAL)
822

    
823
  if not force:
824
    _EnsureInstancesExist(cl, [instance_name])
825

    
826
    if opts.cleanup:
827
      usertext = ("Instance %s will be recovered from a failed migration."
828
                  " Note that the migration procedure (including cleanup)" %
829
                  (instance_name,))
830
    else:
831
      usertext = ("Instance %s will be migrated. Note that migration" %
832
                  (instance_name,))
833
    usertext += (" might impact the instance if anything goes wrong"
834
                 " (e.g. due to bugs in the hypervisor). Continue?")
835
    if not AskUser(usertext):
836
      return 1
837

    
838
  # this should be removed once --non-live is deprecated
839
  if not opts.live and opts.migration_mode is not None:
840
    raise errors.OpPrereqError("Only one of the --non-live and "
841
                               "--migration-mode options can be passed",
842
                               errors.ECODE_INVAL)
843
  if not opts.live: # --non-live passed
844
    mode = constants.HT_MIGRATION_NONLIVE
845
  else:
846
    mode = opts.migration_mode
847

    
848
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
849
                                 cleanup=opts.cleanup, iallocator=iallocator,
850
                                 target_node=target_node,
851
                                 allow_failover=opts.allow_failover,
852
                                 ignore_ipolicy=opts.ignore_ipolicy)
853
  SubmitOpCode(op, cl=cl, opts=opts)
854
  return 0
855

    
856

    
857
def MoveInstance(opts, args):
858
  """Move an instance.
859

860
  @param opts: the command line options selected by the user
861
  @type args: list
862
  @param args: should contain only one element, the instance name
863
  @rtype: int
864
  @return: the desired exit code
865

866
  """
867
  cl = GetClient()
868
  instance_name = args[0]
869
  force = opts.force
870

    
871
  if not force:
872
    usertext = ("Instance %s will be moved."
873
                " This requires a shutdown of the instance. Continue?" %
874
                (instance_name,))
875
    if not AskUser(usertext):
876
      return 1
877

    
878
  op = opcodes.OpInstanceMove(instance_name=instance_name,
879
                              target_node=opts.node,
880
                              shutdown_timeout=opts.shutdown_timeout,
881
                              ignore_consistency=opts.ignore_consistency,
882
                              ignore_ipolicy=opts.ignore_ipolicy)
883
  SubmitOrSend(op, opts, cl=cl)
884
  return 0
885

    
886

    
887
def ConnectToInstanceConsole(opts, args):
888
  """Connect to the console of an instance.
889

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

896
  """
897
  instance_name = args[0]
898

    
899
  cl = GetClient()
900
  try:
901
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
902
    ((console_data, oper_state), ) = \
903
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
904
  finally:
905
    # Ensure client connection is closed while external commands are run
906
    cl.Close()
907

    
908
  del cl
909

    
910
  if not console_data:
911
    if oper_state:
912
      # Instance is running
913
      raise errors.OpExecError("Console information for instance %s is"
914
                               " unavailable" % instance_name)
915
    else:
916
      raise errors.OpExecError("Instance %s is not running, can't get console" %
917
                               instance_name)
918

    
919
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
920
                    opts.show_command, cluster_name)
921

    
922

    
923
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
924
               _runcmd_fn=utils.RunCmd):
925
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
926

927
  @type console: L{objects.InstanceConsole}
928
  @param console: Console object
929
  @type show_command: bool
930
  @param show_command: Whether to just display commands
931
  @type cluster_name: string
932
  @param cluster_name: Cluster name as retrieved from master daemon
933

934
  """
935
  assert console.Validate()
936

    
937
  if console.kind == constants.CONS_MESSAGE:
938
    feedback_fn(console.message)
939
  elif console.kind == constants.CONS_VNC:
940
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
941
                " URL <vnc://%s:%s/>",
942
                console.instance, console.host, console.port,
943
                console.display, console.host, console.port)
944
  elif console.kind == constants.CONS_SPICE:
945
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
946
                console.host, console.port)
947
  elif console.kind == constants.CONS_SSH:
948
    # Convert to string if not already one
949
    if isinstance(console.command, basestring):
950
      cmd = console.command
951
    else:
952
      cmd = utils.ShellQuoteArgs(console.command)
953

    
954
    srun = ssh.SshRunner(cluster_name=cluster_name)
955
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
956
                            batch=True, quiet=False, tty=True)
957

    
958
    if show_command:
959
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
960
    else:
961
      result = _runcmd_fn(ssh_cmd, interactive=True)
962
      if result.failed:
963
        logging.error("Console command \"%s\" failed with reason '%s' and"
964
                      " output %r", result.cmd, result.fail_reason,
965
                      result.output)
966
        raise errors.OpExecError("Connection to console of instance %s failed,"
967
                                 " please check cluster configuration" %
968
                                 console.instance)
969
  else:
970
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
971

    
972
  return constants.EXIT_SUCCESS
973

    
974

    
975
def _FormatLogicalID(dev_type, logical_id, roman):
976
  """Formats the logical_id of a disk.
977

978
  """
979
  if dev_type == constants.LD_DRBD8:
980
    node_a, node_b, port, minor_a, minor_b, key = logical_id
981
    data = [
982
      ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
983
                                                            convert=roman))),
984
      ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
985
                                                            convert=roman))),
986
      ("port", compat.TryToRoman(port, convert=roman)),
987
      ("auth key", key),
988
      ]
989
  elif dev_type == constants.LD_LV:
990
    vg_name, lv_name = logical_id
991
    data = ["%s/%s" % (vg_name, lv_name)]
992
  else:
993
    data = [str(logical_id)]
994

    
995
  return data
996

    
997

    
998
def _FormatBlockDevInfo(idx, top_level, dev, roman):
999
  """Show block device information.
1000

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

1004
  @type idx: int
1005
  @param idx: the index of the current disk
1006
  @type top_level: boolean
1007
  @param top_level: if this a top-level disk?
1008
  @type dev: dict
1009
  @param dev: dictionary with disk information
1010
  @type roman: boolean
1011
  @param roman: whether to try to use roman integers
1012
  @return: a list of either strings, tuples or lists
1013
      (which should be formatted at a higher indent level)
1014

1015
  """
1016
  def helper(dtype, status):
1017
    """Format one line for physical device status.
1018

1019
    @type dtype: str
1020
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
1021
    @type status: tuple
1022
    @param status: a tuple as returned from L{backend.FindBlockDevice}
1023
    @return: the string representing the status
1024

1025
    """
1026
    if not status:
1027
      return "not active"
1028
    txt = ""
1029
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
1030
    if major is None:
1031
      major_string = "N/A"
1032
    else:
1033
      major_string = str(compat.TryToRoman(major, convert=roman))
1034

    
1035
    if minor is None:
1036
      minor_string = "N/A"
1037
    else:
1038
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1039

    
1040
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
1041
    if dtype in (constants.LD_DRBD8, ):
1042
      if syncp is not None:
1043
        sync_text = "*RECOVERING* %5.2f%%," % syncp
1044
        if estt:
1045
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1046
        else:
1047
          sync_text += " ETA unknown"
1048
      else:
1049
        sync_text = "in sync"
1050
      if degr:
1051
        degr_text = "*DEGRADED*"
1052
      else:
1053
        degr_text = "ok"
1054
      if ldisk_status == constants.LDS_FAULTY:
1055
        ldisk_text = " *MISSING DISK*"
1056
      elif ldisk_status == constants.LDS_UNKNOWN:
1057
        ldisk_text = " *UNCERTAIN STATE*"
1058
      else:
1059
        ldisk_text = ""
1060
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1061
    elif dtype == constants.LD_LV:
1062
      if ldisk_status == constants.LDS_FAULTY:
1063
        ldisk_text = " *FAILED* (failed drive?)"
1064
      else:
1065
        ldisk_text = ""
1066
      txt += ldisk_text
1067
    return txt
1068

    
1069
  # the header
1070
  if top_level:
1071
    if dev["iv_name"] is not None:
1072
      txt = dev["iv_name"]
1073
    else:
1074
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1075
  else:
1076
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1077
  if isinstance(dev["size"], int):
1078
    nice_size = utils.FormatUnit(dev["size"], "h")
1079
  else:
1080
    nice_size = dev["size"]
1081
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1082
  data = []
1083
  if top_level:
1084
    data.append(("access mode", dev["mode"]))
1085
  if dev["logical_id"] is not None:
1086
    try:
1087
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1088
    except ValueError:
1089
      l_id = [str(dev["logical_id"])]
1090
    if len(l_id) == 1:
1091
      data.append(("logical_id", l_id[0]))
1092
    else:
1093
      data.extend(l_id)
1094
  elif dev["physical_id"] is not None:
1095
    data.append("physical_id:")
1096
    data.append([dev["physical_id"]])
1097

    
1098
  if dev["pstatus"]:
1099
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1100

    
1101
  if dev["sstatus"]:
1102
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1103

    
1104
  if dev["children"]:
1105
    data.append("child devices:")
1106
    for c_idx, child in enumerate(dev["children"]):
1107
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1108
  d1.append(data)
1109
  return d1
1110

    
1111

    
1112
def _FormatList(buf, data, indent_level):
1113
  """Formats a list of data at a given indent level.
1114

1115
  If the element of the list is:
1116
    - a string, it is simply formatted as is
1117
    - a tuple, it will be split into key, value and the all the
1118
      values in a list will be aligned all at the same start column
1119
    - a list, will be recursively formatted
1120

1121
  @type buf: StringIO
1122
  @param buf: the buffer into which we write the output
1123
  @param data: the list to format
1124
  @type indent_level: int
1125
  @param indent_level: the indent level to format at
1126

1127
  """
1128
  max_tlen = max([len(elem[0]) for elem in data
1129
                 if isinstance(elem, tuple)] or [0])
1130
  for elem in data:
1131
    if isinstance(elem, basestring):
1132
      buf.write("%*s%s\n" % (2 * indent_level, "", elem))
1133
    elif isinstance(elem, tuple):
1134
      key, value = elem
1135
      spacer = "%*s" % (max_tlen - len(key), "")
1136
      buf.write("%*s%s:%s %s\n" % (2 * indent_level, "", key, spacer, value))
1137
    elif isinstance(elem, list):
1138
      _FormatList(buf, elem, indent_level + 1)
1139

    
1140

    
1141
def ShowInstanceConfig(opts, args):
1142
  """Compute instance run-time status.
1143

1144
  @param opts: the command line options selected by the user
1145
  @type args: list
1146
  @param args: either an empty list, and then we query all
1147
      instances, or should contain a list of instance names
1148
  @rtype: int
1149
  @return: the desired exit code
1150

1151
  """
1152
  if not args and not opts.show_all:
1153
    ToStderr("No instance selected."
1154
             " Please pass in --all if you want to query all instances.\n"
1155
             "Note that this can take a long time on a big cluster.")
1156
    return 1
1157
  elif args and opts.show_all:
1158
    ToStderr("Cannot use --all if you specify instance names.")
1159
    return 1
1160

    
1161
  retcode = 0
1162
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1163
                                   use_locking=not opts.static)
1164
  result = SubmitOpCode(op, opts=opts)
1165
  if not result:
1166
    ToStdout("No instances.")
1167
    return 1
1168

    
1169
  buf = StringIO()
1170
  retcode = 0
1171
  for instance_name in result:
1172
    instance = result[instance_name]
1173
    buf.write("Instance name: %s\n" % instance["name"])
1174
    buf.write("UUID: %s\n" % instance["uuid"])
1175
    buf.write("Serial number: %s\n" %
1176
              compat.TryToRoman(instance["serial_no"],
1177
                                convert=opts.roman_integers))
1178
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
1179
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1180
    buf.write("State: configured to be %s" % instance["config_state"])
1181
    if instance["run_state"]:
1182
      buf.write(", actual state is %s" % instance["run_state"])
1183
    buf.write("\n")
1184
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
1185
    ##          instance["auto_balance"])
1186
    buf.write("  Nodes:\n")
1187
    buf.write("    - primary: %s\n" % instance["pnode"])
1188
    buf.write("    - secondaries: %s\n" % utils.CommaJoin(instance["snodes"]))
1189
    buf.write("  Operating system: %s\n" % instance["os"])
1190
    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
1191
                        level=2)
1192
    if "network_port" in instance:
1193
      buf.write("  Allocated network port: %s\n" %
1194
                compat.TryToRoman(instance["network_port"],
1195
                                  convert=opts.roman_integers))
1196
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1197

    
1198
    # custom VNC console information
1199
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1200
                                                 None)
1201
    if vnc_bind_address:
1202
      port = instance["network_port"]
1203
      display = int(port) - constants.VNC_BASE_PORT
1204
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1205
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1206
                                                   port,
1207
                                                   display)
1208
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1209
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1210
                             (vnc_bind_address, port,
1211
                              instance["pnode"], display))
1212
      else:
1213
        # vnc bind address is a file
1214
        vnc_console_port = "%s:%s" % (instance["pnode"],
1215
                                      vnc_bind_address)
1216
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1217

    
1218
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1219
                        level=2)
1220
    buf.write("  Hardware:\n")
1221
    buf.write("    - VCPUs: %s\n" %
1222
              compat.TryToRoman(instance["be_actual"][constants.BE_VCPUS],
1223
                                convert=opts.roman_integers))
1224
    buf.write("    - maxmem: %sMiB\n" %
1225
              compat.TryToRoman(instance["be_actual"][constants.BE_MAXMEM],
1226
                                convert=opts.roman_integers))
1227
    buf.write("    - minmem: %sMiB\n" %
1228
              compat.TryToRoman(instance["be_actual"][constants.BE_MINMEM],
1229
                                convert=opts.roman_integers))
1230
    # deprecated "memory" value, kept for one version for compatibility
1231
    # TODO(ganeti 2.7) remove.
1232
    buf.write("    - memory: %sMiB\n" %
1233
              compat.TryToRoman(instance["be_actual"][constants.BE_MAXMEM],
1234
                                convert=opts.roman_integers))
1235
    buf.write("    - %s: %s\n" %
1236
              (constants.BE_ALWAYS_FAILOVER,
1237
               instance["be_actual"][constants.BE_ALWAYS_FAILOVER]))
1238
    buf.write("    - NICs:\n")
1239
    for idx, (ip, mac, mode, link) in enumerate(instance["nics"]):
1240
      buf.write("      - nic/%d: MAC: %s, IP: %s, mode: %s, link: %s\n" %
1241
                (idx, mac, ip, mode, link))
1242
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1243
    buf.write("  Disks:\n")
1244

    
1245
    for idx, device in enumerate(instance["disks"]):
1246
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1247
                  opts.roman_integers), 2)
1248

    
1249
  ToStdout(buf.getvalue().rstrip("\n"))
1250
  return retcode
1251

    
1252

    
1253
def SetInstanceParams(opts, args):
1254
  """Modifies an instance.
1255

1256
  All parameters take effect only at the next restart of the instance.
1257

1258
  @param opts: the command line options selected by the user
1259
  @type args: list
1260
  @param args: should contain only one element, the instance name
1261
  @rtype: int
1262
  @return: the desired exit code
1263

1264
  """
1265
  if not (opts.nics or opts.disks or opts.disk_template or
1266
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1267
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1268
    ToStderr("Please give at least one of the parameters.")
1269
    return 1
1270

    
1271
  for param in opts.beparams:
1272
    if isinstance(opts.beparams[param], basestring):
1273
      if opts.beparams[param].lower() == "default":
1274
        opts.beparams[param] = constants.VALUE_DEFAULT
1275

    
1276
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1277
                      allowed_values=[constants.VALUE_DEFAULT])
1278

    
1279
  for param in opts.hvparams:
1280
    if isinstance(opts.hvparams[param], basestring):
1281
      if opts.hvparams[param].lower() == "default":
1282
        opts.hvparams[param] = constants.VALUE_DEFAULT
1283

    
1284
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1285
                      allowed_values=[constants.VALUE_DEFAULT])
1286

    
1287
  for idx, (nic_op, nic_dict) in enumerate(opts.nics):
1288
    try:
1289
      nic_op = int(nic_op)
1290
      opts.nics[idx] = (nic_op, nic_dict)
1291
    except (TypeError, ValueError):
1292
      pass
1293

    
1294
  for idx, (disk_op, disk_dict) in enumerate(opts.disks):
1295
    try:
1296
      disk_op = int(disk_op)
1297
      opts.disks[idx] = (disk_op, disk_dict)
1298
    except (TypeError, ValueError):
1299
      pass
1300
    if disk_op == constants.DDM_ADD:
1301
      if "size" not in disk_dict:
1302
        raise errors.OpPrereqError("Missing required parameter 'size'",
1303
                                   errors.ECODE_INVAL)
1304
      disk_dict["size"] = utils.ParseUnit(disk_dict["size"])
1305

    
1306
  if (opts.disk_template and
1307
      opts.disk_template in constants.DTS_INT_MIRROR and
1308
      not opts.node):
1309
    ToStderr("Changing the disk template to a mirrored one requires"
1310
             " specifying a secondary node")
1311
    return 1
1312

    
1313
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1314
                                   nics=opts.nics,
1315
                                   disks=opts.disks,
1316
                                   disk_template=opts.disk_template,
1317
                                   remote_node=opts.node,
1318
                                   hvparams=opts.hvparams,
1319
                                   beparams=opts.beparams,
1320
                                   runtime_mem=opts.runtime_mem,
1321
                                   os_name=opts.os,
1322
                                   osparams=opts.osparams,
1323
                                   force_variant=opts.force_variant,
1324
                                   force=opts.force,
1325
                                   wait_for_sync=opts.wait_for_sync,
1326
                                   offline_inst=opts.offline_inst,
1327
                                   online_inst=opts.online_inst,
1328
                                   ignore_ipolicy=opts.ignore_ipolicy)
1329

    
1330
  # even if here we process the result, we allow submit only
1331
  result = SubmitOrSend(op, opts)
1332

    
1333
  if result:
1334
    ToStdout("Modified instance %s", args[0])
1335
    for param, data in result:
1336
      ToStdout(" - %-5s -> %s", param, data)
1337
    ToStdout("Please don't forget that most parameters take effect"
1338
             " only at the next start of the instance.")
1339
  return 0
1340

    
1341

    
1342
def ChangeGroup(opts, args):
1343
  """Moves an instance to another group.
1344

1345
  """
1346
  (instance_name, ) = args
1347

    
1348
  cl = GetClient()
1349

    
1350
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1351
                                     iallocator=opts.iallocator,
1352
                                     target_groups=opts.to,
1353
                                     early_release=opts.early_release)
1354
  result = SubmitOpCode(op, cl=cl, opts=opts)
1355

    
1356
  # Keep track of submitted jobs
1357
  jex = JobExecutor(cl=cl, opts=opts)
1358

    
1359
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1360
    jex.AddJobId(None, status, job_id)
1361

    
1362
  results = jex.GetResults()
1363
  bad_cnt = len([row for row in results if not row[0]])
1364
  if bad_cnt == 0:
1365
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1366
    rcode = constants.EXIT_SUCCESS
1367
  else:
1368
    ToStdout("There were %s errors while changing group of instance '%s'.",
1369
             bad_cnt, instance_name)
1370
    rcode = constants.EXIT_FAILURE
1371

    
1372
  return rcode
1373

    
1374

    
1375
# multi-instance selection options
1376
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1377
                           help="Do not ask for confirmation when more than"
1378
                           " one instance is affected",
1379
                           action="store_true", default=False)
1380

    
1381
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1382
                            help="Filter by nodes (primary only)",
1383
                            const=_EXPAND_NODES_PRI, action="store_const")
1384

    
1385
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1386
                            help="Filter by nodes (secondary only)",
1387
                            const=_EXPAND_NODES_SEC, action="store_const")
1388

    
1389
m_node_opt = cli_option("--node", dest="multi_mode",
1390
                        help="Filter by nodes (primary and secondary)",
1391
                        const=_EXPAND_NODES_BOTH, action="store_const")
1392

    
1393
m_clust_opt = cli_option("--all", dest="multi_mode",
1394
                         help="Select all instances in the cluster",
1395
                         const=_EXPAND_CLUSTER, action="store_const")
1396

    
1397
m_inst_opt = cli_option("--instance", dest="multi_mode",
1398
                        help="Filter by instance name [default]",
1399
                        const=_EXPAND_INSTANCES, action="store_const")
1400

    
1401
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1402
                             help="Filter by node tag",
1403
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1404
                             action="store_const")
1405

    
1406
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1407
                                 help="Filter by primary node tag",
1408
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1409
                                 action="store_const")
1410

    
1411
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1412
                                 help="Filter by secondary node tag",
1413
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1414
                                 action="store_const")
1415

    
1416
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1417
                             help="Filter by instance tag",
1418
                             const=_EXPAND_INSTANCES_BY_TAGS,
1419
                             action="store_const")
1420

    
1421
# this is defined separately due to readability only
1422
add_opts = [
1423
  NOSTART_OPT,
1424
  OS_OPT,
1425
  FORCE_VARIANT_OPT,
1426
  NO_INSTALL_OPT,
1427
  IGNORE_IPOLICY_OPT,
1428
  ]
1429

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

    
1573
#: dictionary with aliases for commands
1574
aliases = {
1575
  "start": "startup",
1576
  "stop": "shutdown",
1577
  }
1578

    
1579

    
1580
def Main():
1581
  return GenericMain(commands, aliases=aliases,
1582
                     override={"tag_type": constants.TAG_INSTANCE},
1583
                     env_override=_ENV_OVERRIDE)