Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 1fa2c40b

History | View | Annotate | Download (56.3 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

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

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

    
45

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

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

    
62

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

    
68

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

    
72
_INST_DATA_VAL = ht.TListOf(ht.TDict)
73

    
74

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

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

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

97
  """
98
  # pylint: disable=W0142
99

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

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

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

    
150
  return inames
151

    
152

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

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

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

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

    
174

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

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

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

    
208

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

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

218
  """
219
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
220

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

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

    
234

    
235
def ListInstanceFields(opts, args):
236
  """List instance fields.
237

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

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

    
248

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

252
  This is just a wrapper over GenericInstanceCreate.
253

254
  """
255
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
256

    
257

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

261
  This function reads a json file with L{opcodes.OpInstanceCreate}
262
  serialisations.
263

264
  @param opts: the command line options selected by the user
265
  @type args: list
266
  @param args: should contain one element, the json filename
267
  @rtype: int
268
  @return: the desired exit code
269

270
  """
271
  (json_filename,) = args
272
  cl = GetClient()
273

    
274
  try:
275
    instance_data = simplejson.loads(utils.ReadFile(json_filename))
276
  except Exception, err: # pylint: disable=W0703
277
    ToStderr("Can't parse the instance definition file: %s" % str(err))
278
    return 1
279

    
280
  if not _INST_DATA_VAL(instance_data):
281
    ToStderr("The instance definition file is not %s" % _INST_DATA_VAL)
282
    return 1
283

    
284
  instances = []
285
  possible_params = set(opcodes.OpInstanceCreate.GetAllSlots())
286
  for (idx, inst) in enumerate(instance_data):
287
    unknown = set(inst.keys()) - possible_params
288

    
289
    if unknown:
290
      # TODO: Suggest closest match for more user friendly experience
291
      raise errors.OpPrereqError("Unknown fields in definition %s: %s" %
292
                                 (idx, utils.CommaJoin(unknown)),
293
                                 errors.ECODE_INVAL)
294

    
295
    op = opcodes.OpInstanceCreate(**inst) # pylint: disable=W0142
296
    op.Validate(False)
297
    instances.append(op)
298

    
299
  op = opcodes.OpInstanceMultiAlloc(iallocator=opts.iallocator,
300
                                    instances=instances)
301
  result = SubmitOrSend(op, opts, cl=cl)
302

    
303
  # Keep track of submitted jobs
304
  jex = JobExecutor(cl=cl, opts=opts)
305

    
306
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
307
    jex.AddJobId(None, status, job_id)
308

    
309
  results = jex.GetResults()
310
  bad_cnt = len([row for row in results if not row[0]])
311
  if bad_cnt == 0:
312
    ToStdout("All instances created successfully.")
313
    rcode = constants.EXIT_SUCCESS
314
  else:
315
    ToStdout("There were %s errors during the creation.", bad_cnt)
316
    rcode = constants.EXIT_FAILURE
317

    
318
  return rcode
319

    
320

    
321
def ReinstallInstance(opts, args):
322
  """Reinstall an instance.
323

324
  @param opts: the command line options selected by the user
325
  @type args: list
326
  @param args: should contain only one element, the name of the
327
      instance to be reinstalled
328
  @rtype: int
329
  @return: the desired exit code
330

331
  """
332
  # first, compute the desired name list
333
  if opts.multi_mode is None:
334
    opts.multi_mode = _EXPAND_INSTANCES
335

    
336
  inames = _ExpandMultiNames(opts.multi_mode, args)
337
  if not inames:
338
    raise errors.OpPrereqError("Selection filter does not match any instances",
339
                               errors.ECODE_INVAL)
340

    
341
  # second, if requested, ask for an OS
342
  if opts.select_os is True:
343
    op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
344
    result = SubmitOpCode(op, opts=opts)
345

    
346
    if not result:
347
      ToStdout("Can't get the OS list")
348
      return 1
349

    
350
    ToStdout("Available OS templates:")
351
    number = 0
352
    choices = []
353
    for (name, variants) in result:
354
      for entry in CalculateOSNames(name, variants):
355
        ToStdout("%3s: %s", number, entry)
356
        choices.append(("%s" % number, entry, entry))
357
        number += 1
358

    
359
    choices.append(("x", "exit", "Exit gnt-instance reinstall"))
360
    selected = AskUser("Enter OS template number (or x to abort):",
361
                       choices)
362

    
363
    if selected == "exit":
364
      ToStderr("User aborted reinstall, exiting")
365
      return 1
366

    
367
    os_name = selected
368
    os_msg = "change the OS to '%s'" % selected
369
  else:
370
    os_name = opts.os
371
    if opts.os is not None:
372
      os_msg = "change the OS to '%s'" % os_name
373
    else:
374
      os_msg = "keep the same OS"
375

    
376
  # third, get confirmation: multi-reinstall requires --force-multi,
377
  # single-reinstall either --force or --force-multi (--force-multi is
378
  # a stronger --force)
379
  multi_on = opts.multi_mode != _EXPAND_INSTANCES or len(inames) > 1
380
  if multi_on:
381
    warn_msg = ("Note: this will remove *all* data for the"
382
                " below instances! It will %s.\n" % os_msg)
383
    if not (opts.force_multi or
384
            ConfirmOperation(inames, "instances", "reinstall", extra=warn_msg)):
385
      return 1
386
  else:
387
    if not (opts.force or opts.force_multi):
388
      usertext = ("This will reinstall the instance '%s' (and %s) which"
389
                  " removes all data. Continue?") % (inames[0], os_msg)
390
      if not AskUser(usertext):
391
        return 1
392

    
393
  jex = JobExecutor(verbose=multi_on, opts=opts)
394
  for instance_name in inames:
395
    op = opcodes.OpInstanceReinstall(instance_name=instance_name,
396
                                     os_type=os_name,
397
                                     force_variant=opts.force_variant,
398
                                     osparams=opts.osparams)
399
    jex.QueueJob(instance_name, op)
400

    
401
  results = jex.WaitOrShow(not opts.submit_only)
402

    
403
  if compat.all(map(compat.fst, results)):
404
    return constants.EXIT_SUCCESS
405
  else:
406
    return constants.EXIT_FAILURE
407

    
408

    
409
def RemoveInstance(opts, args):
410
  """Remove an instance.
411

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

419
  """
420
  instance_name = args[0]
421
  force = opts.force
422
  cl = GetClient()
423

    
424
  if not force:
425
    _EnsureInstancesExist(cl, [instance_name])
426

    
427
    usertext = ("This will remove the volumes of the instance %s"
428
                " (including mirrors), thus removing all the data"
429
                " of the instance. Continue?") % instance_name
430
    if not AskUser(usertext):
431
      return 1
432

    
433
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
434
                                ignore_failures=opts.ignore_failures,
435
                                shutdown_timeout=opts.shutdown_timeout)
436
  SubmitOrSend(op, opts, cl=cl)
437
  return 0
438

    
439

    
440
def RenameInstance(opts, args):
441
  """Rename an instance.
442

443
  @param opts: the command line options selected by the user
444
  @type args: list
445
  @param args: should contain two elements, the old and the
446
      new instance names
447
  @rtype: int
448
  @return: the desired exit code
449

450
  """
451
  if not opts.name_check:
452
    if not AskUser("As you disabled the check of the DNS entry, please verify"
453
                   " that '%s' is a FQDN. Continue?" % args[1]):
454
      return 1
455

    
456
  op = opcodes.OpInstanceRename(instance_name=args[0],
457
                                new_name=args[1],
458
                                ip_check=opts.ip_check,
459
                                name_check=opts.name_check)
460
  result = SubmitOrSend(op, opts)
461

    
462
  if result:
463
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
464

    
465
  return 0
466

    
467

    
468
def ActivateDisks(opts, args):
469
  """Activate an instance's disks.
470

471
  This serves two purposes:
472
    - it allows (as long as the instance is not running)
473
      mounting the disks and modifying them from the node
474
    - it repairs inactive secondary drbds
475

476
  @param opts: the command line options selected by the user
477
  @type args: list
478
  @param args: should contain only one element, the instance name
479
  @rtype: int
480
  @return: the desired exit code
481

482
  """
483
  instance_name = args[0]
484
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
485
                                       ignore_size=opts.ignore_size,
486
                                       wait_for_sync=opts.wait_for_sync)
487
  disks_info = SubmitOrSend(op, opts)
488
  for host, iname, nname in disks_info:
489
    ToStdout("%s:%s:%s", host, iname, nname)
490
  return 0
491

    
492

    
493
def DeactivateDisks(opts, args):
494
  """Deactivate an instance's disks.
495

496
  This function takes the instance name, looks for its primary node
497
  and the tries to shutdown its block devices on that node.
498

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

505
  """
506
  instance_name = args[0]
507
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
508
                                         force=opts.force)
509
  SubmitOrSend(op, opts)
510
  return 0
511

    
512

    
513
def RecreateDisks(opts, args):
514
  """Recreate an instance's disks.
515

516
  @param opts: the command line options selected by the user
517
  @type args: list
518
  @param args: should contain only one element, the instance name
519
  @rtype: int
520
  @return: the desired exit code
521

522
  """
523
  instance_name = args[0]
524

    
525
  disks = []
526

    
527
  if opts.disks:
528
    for didx, ddict in opts.disks:
529
      didx = int(didx)
530

    
531
      if not ht.TDict(ddict):
532
        msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
533
        raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
534

    
535
      if constants.IDISK_SIZE in ddict:
536
        try:
537
          ddict[constants.IDISK_SIZE] = \
538
            utils.ParseUnit(ddict[constants.IDISK_SIZE])
539
        except ValueError, err:
540
          raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
541
                                     (didx, err), errors.ECODE_INVAL)
542

    
543
      disks.append((didx, ddict))
544

    
545
    # TODO: Verify modifyable parameters (already done in
546
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
547

    
548
  if opts.node:
549
    if opts.iallocator:
550
      msg = "At most one of either --nodes or --iallocator can be passed"
551
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
552
    pnode, snode = SplitNodeOption(opts.node)
553
    nodes = [pnode]
554
    if snode is not None:
555
      nodes.append(snode)
556
  else:
557
    nodes = []
558

    
559
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
560
                                       disks=disks, nodes=nodes,
561
                                       iallocator=opts.iallocator)
562
  SubmitOrSend(op, opts)
563

    
564
  return 0
565

    
566

    
567
def GrowDisk(opts, args):
568
  """Grow an instance's disks.
569

570
  @param opts: the command line options selected by the user
571
  @type args: list
572
  @param args: should contain three elements, the target instance name,
573
      the target disk id, and the target growth
574
  @rtype: int
575
  @return: the desired exit code
576

577
  """
578
  instance = args[0]
579
  disk = args[1]
580
  try:
581
    disk = int(disk)
582
  except (TypeError, ValueError), err:
583
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
584
                               errors.ECODE_INVAL)
585
  try:
586
    amount = utils.ParseUnit(args[2])
587
  except errors.UnitParseError:
588
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
589
                               errors.ECODE_INVAL)
590
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
591
                                  disk=disk, amount=amount,
592
                                  wait_for_sync=opts.wait_for_sync,
593
                                  absolute=opts.absolute)
594
  SubmitOrSend(op, opts)
595
  return 0
596

    
597

    
598
def _StartupInstance(name, opts):
599
  """Startup instances.
600

601
  This returns the opcode to start an instance, and its decorator will
602
  wrap this into a loop starting all desired instances.
603

604
  @param name: the name of the instance to act on
605
  @param opts: the command line options selected by the user
606
  @return: the opcode needed for the operation
607

608
  """
609
  op = opcodes.OpInstanceStartup(instance_name=name,
610
                                 force=opts.force,
611
                                 ignore_offline_nodes=opts.ignore_offline,
612
                                 no_remember=opts.no_remember,
613
                                 startup_paused=opts.startup_paused)
614
  # do not add these parameters to the opcode unless they're defined
615
  if opts.hvparams:
616
    op.hvparams = opts.hvparams
617
  if opts.beparams:
618
    op.beparams = opts.beparams
619
  return op
620

    
621

    
622
def _RebootInstance(name, opts):
623
  """Reboot instance(s).
624

625
  This returns the opcode to reboot an instance, and its decorator
626
  will wrap this into a loop rebooting all desired instances.
627

628
  @param name: the name of the instance to act on
629
  @param opts: the command line options selected by the user
630
  @return: the opcode needed for the operation
631

632
  """
633
  return opcodes.OpInstanceReboot(instance_name=name,
634
                                  reboot_type=opts.reboot_type,
635
                                  ignore_secondaries=opts.ignore_secondaries,
636
                                  shutdown_timeout=opts.shutdown_timeout)
637

    
638

    
639
def _ShutdownInstance(name, opts):
640
  """Shutdown an instance.
641

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

645
  @param name: the name of the instance to act on
646
  @param opts: the command line options selected by the user
647
  @return: the opcode needed for the operation
648

649
  """
650
  return opcodes.OpInstanceShutdown(instance_name=name,
651
                                    timeout=opts.timeout,
652
                                    ignore_offline_nodes=opts.ignore_offline,
653
                                    no_remember=opts.no_remember)
654

    
655

    
656
def ReplaceDisks(opts, args):
657
  """Replace the disks of an instance
658

659
  @param opts: the command line options selected by the user
660
  @type args: list
661
  @param args: should contain only one element, the instance name
662
  @rtype: int
663
  @return: the desired exit code
664

665
  """
666
  new_2ndary = opts.dst_node
667
  iallocator = opts.iallocator
668
  if opts.disks is None:
669
    disks = []
670
  else:
671
    try:
672
      disks = [int(i) for i in opts.disks.split(",")]
673
    except (TypeError, ValueError), err:
674
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
675
                                 errors.ECODE_INVAL)
676
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
677
         new_2ndary is not None, iallocator is not None].count(True)
678
  if cnt != 1:
679
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
680
                               " options must be passed", errors.ECODE_INVAL)
681
  elif opts.on_primary:
682
    mode = constants.REPLACE_DISK_PRI
683
  elif opts.on_secondary:
684
    mode = constants.REPLACE_DISK_SEC
685
  elif opts.auto:
686
    mode = constants.REPLACE_DISK_AUTO
687
    if disks:
688
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
689
                                 " mode", errors.ECODE_INVAL)
690
  elif new_2ndary is not None or iallocator is not None:
691
    # replace secondary
692
    mode = constants.REPLACE_DISK_CHG
693

    
694
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
695
                                      remote_node=new_2ndary, mode=mode,
696
                                      iallocator=iallocator,
697
                                      early_release=opts.early_release,
698
                                      ignore_ipolicy=opts.ignore_ipolicy)
699
  SubmitOrSend(op, opts)
700
  return 0
701

    
702

    
703
def FailoverInstance(opts, args):
704
  """Failover an instance.
705

706
  The failover is done by shutting it down on its present node and
707
  starting it on the secondary.
708

709
  @param opts: the command line options selected by the user
710
  @type args: list
711
  @param args: should contain only one element, the instance name
712
  @rtype: int
713
  @return: the desired exit code
714

715
  """
716
  cl = GetClient()
717
  instance_name = args[0]
718
  force = opts.force
719
  iallocator = opts.iallocator
720
  target_node = opts.dst_node
721

    
722
  if iallocator and target_node:
723
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
724
                               " node (-n) but not both", errors.ECODE_INVAL)
725

    
726
  if not force:
727
    _EnsureInstancesExist(cl, [instance_name])
728

    
729
    usertext = ("Failover will happen to image %s."
730
                " This requires a shutdown of the instance. Continue?" %
731
                (instance_name,))
732
    if not AskUser(usertext):
733
      return 1
734

    
735
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
736
                                  ignore_consistency=opts.ignore_consistency,
737
                                  shutdown_timeout=opts.shutdown_timeout,
738
                                  iallocator=iallocator,
739
                                  target_node=target_node,
740
                                  ignore_ipolicy=opts.ignore_ipolicy)
741
  SubmitOrSend(op, opts, cl=cl)
742
  return 0
743

    
744

    
745
def MigrateInstance(opts, args):
746
  """Migrate an instance.
747

748
  The migrate is done without shutdown.
749

750
  @param opts: the command line options selected by the user
751
  @type args: list
752
  @param args: should contain only one element, the instance name
753
  @rtype: int
754
  @return: the desired exit code
755

756
  """
757
  cl = GetClient()
758
  instance_name = args[0]
759
  force = opts.force
760
  iallocator = opts.iallocator
761
  target_node = opts.dst_node
762

    
763
  if iallocator and target_node:
764
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
765
                               " node (-n) but not both", errors.ECODE_INVAL)
766

    
767
  if not force:
768
    _EnsureInstancesExist(cl, [instance_name])
769

    
770
    if opts.cleanup:
771
      usertext = ("Instance %s will be recovered from a failed migration."
772
                  " Note that the migration procedure (including cleanup)" %
773
                  (instance_name,))
774
    else:
775
      usertext = ("Instance %s will be migrated. Note that migration" %
776
                  (instance_name,))
777
    usertext += (" might impact the instance if anything goes wrong"
778
                 " (e.g. due to bugs in the hypervisor). Continue?")
779
    if not AskUser(usertext):
780
      return 1
781

    
782
  # this should be removed once --non-live is deprecated
783
  if not opts.live and opts.migration_mode is not None:
784
    raise errors.OpPrereqError("Only one of the --non-live and "
785
                               "--migration-mode options can be passed",
786
                               errors.ECODE_INVAL)
787
  if not opts.live: # --non-live passed
788
    mode = constants.HT_MIGRATION_NONLIVE
789
  else:
790
    mode = opts.migration_mode
791

    
792
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
793
                                 cleanup=opts.cleanup, iallocator=iallocator,
794
                                 target_node=target_node,
795
                                 allow_failover=opts.allow_failover,
796
                                 allow_runtime_changes=opts.allow_runtime_chgs,
797
                                 ignore_ipolicy=opts.ignore_ipolicy)
798
  SubmitOrSend(op, cl=cl, opts=opts)
799
  return 0
800

    
801

    
802
def MoveInstance(opts, args):
803
  """Move an instance.
804

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

811
  """
812
  cl = GetClient()
813
  instance_name = args[0]
814
  force = opts.force
815

    
816
  if not force:
817
    usertext = ("Instance %s will be moved."
818
                " This requires a shutdown of the instance. Continue?" %
819
                (instance_name,))
820
    if not AskUser(usertext):
821
      return 1
822

    
823
  op = opcodes.OpInstanceMove(instance_name=instance_name,
824
                              target_node=opts.node,
825
                              shutdown_timeout=opts.shutdown_timeout,
826
                              ignore_consistency=opts.ignore_consistency,
827
                              ignore_ipolicy=opts.ignore_ipolicy)
828
  SubmitOrSend(op, opts, cl=cl)
829
  return 0
830

    
831

    
832
def ConnectToInstanceConsole(opts, args):
833
  """Connect to the console of an instance.
834

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

841
  """
842
  instance_name = args[0]
843

    
844
  cl = GetClient()
845
  try:
846
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
847
    ((console_data, oper_state), ) = \
848
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
849
  finally:
850
    # Ensure client connection is closed while external commands are run
851
    cl.Close()
852

    
853
  del cl
854

    
855
  if not console_data:
856
    if oper_state:
857
      # Instance is running
858
      raise errors.OpExecError("Console information for instance %s is"
859
                               " unavailable" % instance_name)
860
    else:
861
      raise errors.OpExecError("Instance %s is not running, can't get console" %
862
                               instance_name)
863

    
864
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
865
                    opts.show_command, cluster_name)
866

    
867

    
868
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
869
               _runcmd_fn=utils.RunCmd):
870
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
871

872
  @type console: L{objects.InstanceConsole}
873
  @param console: Console object
874
  @type show_command: bool
875
  @param show_command: Whether to just display commands
876
  @type cluster_name: string
877
  @param cluster_name: Cluster name as retrieved from master daemon
878

879
  """
880
  assert console.Validate()
881

    
882
  if console.kind == constants.CONS_MESSAGE:
883
    feedback_fn(console.message)
884
  elif console.kind == constants.CONS_VNC:
885
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
886
                " URL <vnc://%s:%s/>",
887
                console.instance, console.host, console.port,
888
                console.display, console.host, console.port)
889
  elif console.kind == constants.CONS_SPICE:
890
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
891
                console.host, console.port)
892
  elif console.kind == constants.CONS_SSH:
893
    # Convert to string if not already one
894
    if isinstance(console.command, basestring):
895
      cmd = console.command
896
    else:
897
      cmd = utils.ShellQuoteArgs(console.command)
898

    
899
    srun = ssh.SshRunner(cluster_name=cluster_name)
900
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
901
                            batch=True, quiet=False, tty=True)
902

    
903
    if show_command:
904
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
905
    else:
906
      result = _runcmd_fn(ssh_cmd, interactive=True)
907
      if result.failed:
908
        logging.error("Console command \"%s\" failed with reason '%s' and"
909
                      " output %r", result.cmd, result.fail_reason,
910
                      result.output)
911
        raise errors.OpExecError("Connection to console of instance %s failed,"
912
                                 " please check cluster configuration" %
913
                                 console.instance)
914
  else:
915
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
916

    
917
  return constants.EXIT_SUCCESS
918

    
919

    
920
def _FormatLogicalID(dev_type, logical_id, roman):
921
  """Formats the logical_id of a disk.
922

923
  """
924
  if dev_type == constants.LD_DRBD8:
925
    node_a, node_b, port, minor_a, minor_b, key = logical_id
926
    data = [
927
      ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
928
                                                            convert=roman))),
929
      ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
930
                                                            convert=roman))),
931
      ("port", compat.TryToRoman(port, convert=roman)),
932
      ("auth key", key),
933
      ]
934
  elif dev_type == constants.LD_LV:
935
    vg_name, lv_name = logical_id
936
    data = ["%s/%s" % (vg_name, lv_name)]
937
  else:
938
    data = [str(logical_id)]
939

    
940
  return data
941

    
942

    
943
def _FormatBlockDevInfo(idx, top_level, dev, roman):
944
  """Show block device information.
945

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

949
  @type idx: int
950
  @param idx: the index of the current disk
951
  @type top_level: boolean
952
  @param top_level: if this a top-level disk?
953
  @type dev: dict
954
  @param dev: dictionary with disk information
955
  @type roman: boolean
956
  @param roman: whether to try to use roman integers
957
  @return: a list of either strings, tuples or lists
958
      (which should be formatted at a higher indent level)
959

960
  """
961
  def helper(dtype, status):
962
    """Format one line for physical device status.
963

964
    @type dtype: str
965
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
966
    @type status: tuple
967
    @param status: a tuple as returned from L{backend.FindBlockDevice}
968
    @return: the string representing the status
969

970
    """
971
    if not status:
972
      return "not active"
973
    txt = ""
974
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
975
    if major is None:
976
      major_string = "N/A"
977
    else:
978
      major_string = str(compat.TryToRoman(major, convert=roman))
979

    
980
    if minor is None:
981
      minor_string = "N/A"
982
    else:
983
      minor_string = str(compat.TryToRoman(minor, convert=roman))
984

    
985
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
986
    if dtype in (constants.LD_DRBD8, ):
987
      if syncp is not None:
988
        sync_text = "*RECOVERING* %5.2f%%," % syncp
989
        if estt:
990
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
991
        else:
992
          sync_text += " ETA unknown"
993
      else:
994
        sync_text = "in sync"
995
      if degr:
996
        degr_text = "*DEGRADED*"
997
      else:
998
        degr_text = "ok"
999
      if ldisk_status == constants.LDS_FAULTY:
1000
        ldisk_text = " *MISSING DISK*"
1001
      elif ldisk_status == constants.LDS_UNKNOWN:
1002
        ldisk_text = " *UNCERTAIN STATE*"
1003
      else:
1004
        ldisk_text = ""
1005
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1006
    elif dtype == constants.LD_LV:
1007
      if ldisk_status == constants.LDS_FAULTY:
1008
        ldisk_text = " *FAILED* (failed drive?)"
1009
      else:
1010
        ldisk_text = ""
1011
      txt += ldisk_text
1012
    return txt
1013

    
1014
  # the header
1015
  if top_level:
1016
    if dev["iv_name"] is not None:
1017
      txt = dev["iv_name"]
1018
    else:
1019
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1020
  else:
1021
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1022
  if isinstance(dev["size"], int):
1023
    nice_size = utils.FormatUnit(dev["size"], "h")
1024
  else:
1025
    nice_size = dev["size"]
1026
  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
1027
  data = []
1028
  if top_level:
1029
    data.append(("access mode", dev["mode"]))
1030
  if dev["logical_id"] is not None:
1031
    try:
1032
      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"], roman)
1033
    except ValueError:
1034
      l_id = [str(dev["logical_id"])]
1035
    if len(l_id) == 1:
1036
      data.append(("logical_id", l_id[0]))
1037
    else:
1038
      data.extend(l_id)
1039
  elif dev["physical_id"] is not None:
1040
    data.append("physical_id:")
1041
    data.append([dev["physical_id"]])
1042

    
1043
  if dev["pstatus"]:
1044
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1045

    
1046
  if dev["sstatus"]:
1047
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1048

    
1049
  if dev["children"]:
1050
    data.append("child devices:")
1051
    for c_idx, child in enumerate(dev["children"]):
1052
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1053
  d1.append(data)
1054
  return d1
1055

    
1056

    
1057
def _FormatList(buf, data, indent_level):
1058
  """Formats a list of data at a given indent level.
1059

1060
  If the element of the list is:
1061
    - a string, it is simply formatted as is
1062
    - a tuple, it will be split into key, value and the all the
1063
      values in a list will be aligned all at the same start column
1064
    - a list, will be recursively formatted
1065

1066
  @type buf: StringIO
1067
  @param buf: the buffer into which we write the output
1068
  @param data: the list to format
1069
  @type indent_level: int
1070
  @param indent_level: the indent level to format at
1071

1072
  """
1073
  max_tlen = max([len(elem[0]) for elem in data
1074
                 if isinstance(elem, tuple)] or [0])
1075
  for elem in data:
1076
    if isinstance(elem, basestring):
1077
      buf.write("%*s%s\n" % (2 * indent_level, "", elem))
1078
    elif isinstance(elem, tuple):
1079
      key, value = elem
1080
      spacer = "%*s" % (max_tlen - len(key), "")
1081
      buf.write("%*s%s:%s %s\n" % (2 * indent_level, "", key, spacer, value))
1082
    elif isinstance(elem, list):
1083
      _FormatList(buf, elem, indent_level + 1)
1084

    
1085

    
1086
def ShowInstanceConfig(opts, args):
1087
  """Compute instance run-time status.
1088

1089
  @param opts: the command line options selected by the user
1090
  @type args: list
1091
  @param args: either an empty list, and then we query all
1092
      instances, or should contain a list of instance names
1093
  @rtype: int
1094
  @return: the desired exit code
1095

1096
  """
1097
  if not args and not opts.show_all:
1098
    ToStderr("No instance selected."
1099
             " Please pass in --all if you want to query all instances.\n"
1100
             "Note that this can take a long time on a big cluster.")
1101
    return 1
1102
  elif args and opts.show_all:
1103
    ToStderr("Cannot use --all if you specify instance names.")
1104
    return 1
1105

    
1106
  retcode = 0
1107
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1108
                                   use_locking=not opts.static)
1109
  result = SubmitOpCode(op, opts=opts)
1110
  if not result:
1111
    ToStdout("No instances.")
1112
    return 1
1113

    
1114
  buf = StringIO()
1115
  retcode = 0
1116
  for instance_name in result:
1117
    instance = result[instance_name]
1118
    buf.write("Instance name: %s\n" % instance["name"])
1119
    buf.write("UUID: %s\n" % instance["uuid"])
1120
    buf.write("Serial number: %s\n" %
1121
              compat.TryToRoman(instance["serial_no"],
1122
                                convert=opts.roman_integers))
1123
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
1124
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1125
    buf.write("State: configured to be %s" % instance["config_state"])
1126
    if instance["run_state"]:
1127
      buf.write(", actual state is %s" % instance["run_state"])
1128
    buf.write("\n")
1129
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
1130
    ##          instance["auto_balance"])
1131
    buf.write("  Nodes:\n")
1132
    buf.write("    - primary: %s\n" % instance["pnode"])
1133
    buf.write("      group: %s (UUID %s)\n" %
1134
              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1135
    buf.write("    - secondaries: %s\n" %
1136
              utils.CommaJoin("%s (group %s, group UUID %s)" %
1137
                                (name, group_name, group_uuid)
1138
                              for (name, group_name, group_uuid) in
1139
                                zip(instance["snodes"],
1140
                                    instance["snodes_group_names"],
1141
                                    instance["snodes_group_uuids"])))
1142
    buf.write("  Operating system: %s\n" % instance["os"])
1143
    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
1144
                        level=2)
1145
    if "network_port" in instance:
1146
      buf.write("  Allocated network port: %s\n" %
1147
                compat.TryToRoman(instance["network_port"],
1148
                                  convert=opts.roman_integers))
1149
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1150

    
1151
    # custom VNC console information
1152
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1153
                                                 None)
1154
    if vnc_bind_address:
1155
      port = instance["network_port"]
1156
      display = int(port) - constants.VNC_BASE_PORT
1157
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1158
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1159
                                                   port,
1160
                                                   display)
1161
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1162
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1163
                             (vnc_bind_address, port,
1164
                              instance["pnode"], display))
1165
      else:
1166
        # vnc bind address is a file
1167
        vnc_console_port = "%s:%s" % (instance["pnode"],
1168
                                      vnc_bind_address)
1169
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1170

    
1171
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1172
                        level=2)
1173
    buf.write("  Hardware:\n")
1174
    # deprecated "memory" value, kept for one version for compatibility
1175
    # TODO(ganeti 2.7) remove.
1176
    be_actual = copy.deepcopy(instance["be_actual"])
1177
    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1178
    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
1179
    # TODO(ganeti 2.7) rework the NICs as well
1180
    buf.write("    - NICs:\n")
1181
    for idx, (ip, mac, mode, link, network, _) in enumerate(instance["nics"]):
1182
      buf.write("      - nic/%d: MAC: %s, IP: %s,"
1183
                " mode: %s, link: %s, network: %s\n" %
1184
                (idx, mac, ip, mode, link, network))
1185
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1186
    buf.write("  Disks:\n")
1187

    
1188
    for idx, device in enumerate(instance["disks"]):
1189
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1190
                  opts.roman_integers), 2)
1191

    
1192
  ToStdout(buf.getvalue().rstrip("\n"))
1193
  return retcode
1194

    
1195

    
1196
def _ConvertNicDiskModifications(mods):
1197
  """Converts NIC/disk modifications from CLI to opcode.
1198

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

1204
  @type mods: list of tuples
1205
  @param mods: Modifications as given by command line parser
1206
  @rtype: list of tuples
1207
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1208

1209
  """
1210
  result = []
1211

    
1212
  for (idx, params) in mods:
1213
    if idx == constants.DDM_ADD:
1214
      # Add item as last item (legacy interface)
1215
      action = constants.DDM_ADD
1216
      idxno = -1
1217
    elif idx == constants.DDM_REMOVE:
1218
      # Remove last item (legacy interface)
1219
      action = constants.DDM_REMOVE
1220
      idxno = -1
1221
    else:
1222
      # Modifications and adding/removing at arbitrary indices
1223
      try:
1224
        idxno = int(idx)
1225
      except (TypeError, ValueError):
1226
        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
1227
                                   errors.ECODE_INVAL)
1228

    
1229
      add = params.pop(constants.DDM_ADD, _MISSING)
1230
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1231
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1232

    
1233
      if modify is _MISSING:
1234
        if not (add is _MISSING or remove is _MISSING):
1235
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1236
                                     errors.ECODE_INVAL)
1237
        elif add is not _MISSING:
1238
          action = constants.DDM_ADD
1239
        elif remove is not _MISSING:
1240
          action = constants.DDM_REMOVE
1241
        else:
1242
          action = constants.DDM_MODIFY
1243

    
1244
      elif add is _MISSING and remove is _MISSING:
1245
        action = constants.DDM_MODIFY
1246
      else:
1247
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1248
                                   " same time", errors.ECODE_INVAL)
1249

    
1250
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1251

    
1252
    if action == constants.DDM_REMOVE and params:
1253
      raise errors.OpPrereqError("Not accepting parameters on removal",
1254
                                 errors.ECODE_INVAL)
1255

    
1256
    result.append((action, idxno, params))
1257

    
1258
  return result
1259

    
1260

    
1261
def _ParseDiskSizes(mods):
1262
  """Parses disk sizes in parameters.
1263

1264
  """
1265
  for (action, _, params) in mods:
1266
    if params and constants.IDISK_SIZE in params:
1267
      params[constants.IDISK_SIZE] = \
1268
        utils.ParseUnit(params[constants.IDISK_SIZE])
1269
    elif action == constants.DDM_ADD:
1270
      raise errors.OpPrereqError("Missing required parameter 'size'",
1271
                                 errors.ECODE_INVAL)
1272

    
1273
  return mods
1274

    
1275

    
1276
def SetInstanceParams(opts, args):
1277
  """Modifies an instance.
1278

1279
  All parameters take effect only at the next restart of the instance.
1280

1281
  @param opts: the command line options selected by the user
1282
  @type args: list
1283
  @param args: should contain only one element, the instance name
1284
  @rtype: int
1285
  @return: the desired exit code
1286

1287
  """
1288
  if not (opts.nics or opts.disks or opts.disk_template or
1289
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1290
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1291
    ToStderr("Please give at least one of the parameters.")
1292
    return 1
1293

    
1294
  for param in opts.beparams:
1295
    if isinstance(opts.beparams[param], basestring):
1296
      if opts.beparams[param].lower() == "default":
1297
        opts.beparams[param] = constants.VALUE_DEFAULT
1298

    
1299
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1300
                      allowed_values=[constants.VALUE_DEFAULT])
1301

    
1302
  for param in opts.hvparams:
1303
    if isinstance(opts.hvparams[param], basestring):
1304
      if opts.hvparams[param].lower() == "default":
1305
        opts.hvparams[param] = constants.VALUE_DEFAULT
1306

    
1307
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1308
                      allowed_values=[constants.VALUE_DEFAULT])
1309

    
1310
  nics = _ConvertNicDiskModifications(opts.nics)
1311
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1312

    
1313
  if (opts.disk_template and
1314
      opts.disk_template in constants.DTS_INT_MIRROR and
1315
      not opts.node):
1316
    ToStderr("Changing the disk template to a mirrored one requires"
1317
             " specifying a secondary node")
1318
    return 1
1319

    
1320
  if opts.offline_inst:
1321
    offline = True
1322
  elif opts.online_inst:
1323
    offline = False
1324
  else:
1325
    offline = None
1326

    
1327
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1328
                                   nics=nics,
1329
                                   disks=disks,
1330
                                   disk_template=opts.disk_template,
1331
                                   remote_node=opts.node,
1332
                                   hvparams=opts.hvparams,
1333
                                   beparams=opts.beparams,
1334
                                   runtime_mem=opts.runtime_mem,
1335
                                   os_name=opts.os,
1336
                                   osparams=opts.osparams,
1337
                                   force_variant=opts.force_variant,
1338
                                   force=opts.force,
1339
                                   wait_for_sync=opts.wait_for_sync,
1340
                                   offline=offline,
1341
                                   conflicts_check=opts.conflicts_check,
1342
                                   ignore_ipolicy=opts.ignore_ipolicy)
1343

    
1344
  # even if here we process the result, we allow submit only
1345
  result = SubmitOrSend(op, opts)
1346

    
1347
  if result:
1348
    ToStdout("Modified instance %s", args[0])
1349
    for param, data in result:
1350
      ToStdout(" - %-5s -> %s", param, data)
1351
    ToStdout("Please don't forget that most parameters take effect"
1352
             " only at the next (re)start of the instance initiated by"
1353
             " ganeti; restarting from within the instance will"
1354
             " not be enough.")
1355
  return 0
1356

    
1357

    
1358
def ChangeGroup(opts, args):
1359
  """Moves an instance to another group.
1360

1361
  """
1362
  (instance_name, ) = args
1363

    
1364
  cl = GetClient()
1365

    
1366
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1367
                                     iallocator=opts.iallocator,
1368
                                     target_groups=opts.to,
1369
                                     early_release=opts.early_release)
1370
  result = SubmitOrSend(op, opts, cl=cl)
1371

    
1372
  # Keep track of submitted jobs
1373
  jex = JobExecutor(cl=cl, opts=opts)
1374

    
1375
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1376
    jex.AddJobId(None, status, job_id)
1377

    
1378
  results = jex.GetResults()
1379
  bad_cnt = len([row for row in results if not row[0]])
1380
  if bad_cnt == 0:
1381
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1382
    rcode = constants.EXIT_SUCCESS
1383
  else:
1384
    ToStdout("There were %s errors while changing group of instance '%s'.",
1385
             bad_cnt, instance_name)
1386
    rcode = constants.EXIT_FAILURE
1387

    
1388
  return rcode
1389

    
1390

    
1391
# multi-instance selection options
1392
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1393
                           help="Do not ask for confirmation when more than"
1394
                           " one instance is affected",
1395
                           action="store_true", default=False)
1396

    
1397
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1398
                            help="Filter by nodes (primary only)",
1399
                            const=_EXPAND_NODES_PRI, action="store_const")
1400

    
1401
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1402
                            help="Filter by nodes (secondary only)",
1403
                            const=_EXPAND_NODES_SEC, action="store_const")
1404

    
1405
m_node_opt = cli_option("--node", dest="multi_mode",
1406
                        help="Filter by nodes (primary and secondary)",
1407
                        const=_EXPAND_NODES_BOTH, action="store_const")
1408

    
1409
m_clust_opt = cli_option("--all", dest="multi_mode",
1410
                         help="Select all instances in the cluster",
1411
                         const=_EXPAND_CLUSTER, action="store_const")
1412

    
1413
m_inst_opt = cli_option("--instance", dest="multi_mode",
1414
                        help="Filter by instance name [default]",
1415
                        const=_EXPAND_INSTANCES, action="store_const")
1416

    
1417
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1418
                             help="Filter by node tag",
1419
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1420
                             action="store_const")
1421

    
1422
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1423
                                 help="Filter by primary node tag",
1424
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1425
                                 action="store_const")
1426

    
1427
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1428
                                 help="Filter by secondary node tag",
1429
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1430
                                 action="store_const")
1431

    
1432
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1433
                             help="Filter by instance tag",
1434
                             const=_EXPAND_INSTANCES_BY_TAGS,
1435
                             action="store_const")
1436

    
1437
# this is defined separately due to readability only
1438
add_opts = [
1439
  NOSTART_OPT,
1440
  OS_OPT,
1441
  FORCE_VARIANT_OPT,
1442
  NO_INSTALL_OPT,
1443
  IGNORE_IPOLICY_OPT,
1444
  ]
1445

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

    
1592
#: dictionary with aliases for commands
1593
aliases = {
1594
  "start": "startup",
1595
  "stop": "shutdown",
1596
  "show": "info",
1597
  }
1598

    
1599

    
1600
def Main():
1601
  return GenericMain(commands, aliases=aliases,
1602
                     override={"tag_type": constants.TAG_INSTANCE},
1603
                     env_override=_ENV_OVERRIDE)