Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 981224e9

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

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

    
44

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

    
55
_EXPAND_NODES_TAGS_MODES = compat.UniqueFrozenset([
56
  _EXPAND_NODES_BOTH_BY_TAGS,
57
  _EXPAND_NODES_PRI_BY_TAGS,
58
  _EXPAND_NODES_SEC_BY_TAGS,
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
_MISSING = object()
67
_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
68

    
69
_INST_DATA_VAL = ht.TListOf(ht.TDict)
70

    
71

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

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

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

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

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

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

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

    
147
  return inames
148

    
149

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

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

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

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

    
171

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

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

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

    
206

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

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

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

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

    
227
  cl = GetClient(query=True)
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, cl=cl)
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
      if constants.IDISK_SPINDLES in ddict:
544
        try:
545
          ddict[constants.IDISK_SPINDLES] = \
546
              int(ddict[constants.IDISK_SPINDLES])
547
        except ValueError, err:
548
          raise errors.OpPrereqError("Invalid spindles for disk %d: %s" %
549
                                     (didx, err), errors.ECODE_INVAL)
550

    
551
      disks.append((didx, ddict))
552

    
553
    # TODO: Verify modifyable parameters (already done in
554
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
555

    
556
  if opts.node:
557
    if opts.iallocator:
558
      msg = "At most one of either --nodes or --iallocator can be passed"
559
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
560
    pnode, snode = SplitNodeOption(opts.node)
561
    nodes = [pnode]
562
    if snode is not None:
563
      nodes.append(snode)
564
  else:
565
    nodes = []
566

    
567
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
568
                                       disks=disks, nodes=nodes,
569
                                       iallocator=opts.iallocator)
570
  SubmitOrSend(op, opts)
571

    
572
  return 0
573

    
574

    
575
def GrowDisk(opts, args):
576
  """Grow an instance's disks.
577

578
  @param opts: the command line options selected by the user
579
  @type args: list
580
  @param args: should contain three elements, the target instance name,
581
      the target disk id, and the target growth
582
  @rtype: int
583
  @return: the desired exit code
584

585
  """
586
  instance = args[0]
587
  disk = args[1]
588
  try:
589
    disk = int(disk)
590
  except (TypeError, ValueError), err:
591
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
592
                               errors.ECODE_INVAL)
593
  try:
594
    amount = utils.ParseUnit(args[2])
595
  except errors.UnitParseError:
596
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
597
                               errors.ECODE_INVAL)
598
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
599
                                  disk=disk, amount=amount,
600
                                  wait_for_sync=opts.wait_for_sync,
601
                                  absolute=opts.absolute)
602
  SubmitOrSend(op, opts)
603
  return 0
604

    
605

    
606
def _StartupInstance(name, opts):
607
  """Startup instances.
608

609
  This returns the opcode to start an instance, and its decorator will
610
  wrap this into a loop starting all desired instances.
611

612
  @param name: the name of the instance to act on
613
  @param opts: the command line options selected by the user
614
  @return: the opcode needed for the operation
615

616
  """
617
  op = opcodes.OpInstanceStartup(instance_name=name,
618
                                 force=opts.force,
619
                                 ignore_offline_nodes=opts.ignore_offline,
620
                                 no_remember=opts.no_remember,
621
                                 startup_paused=opts.startup_paused)
622
  # do not add these parameters to the opcode unless they're defined
623
  if opts.hvparams:
624
    op.hvparams = opts.hvparams
625
  if opts.beparams:
626
    op.beparams = opts.beparams
627
  return op
628

    
629

    
630
def _RebootInstance(name, opts):
631
  """Reboot instance(s).
632

633
  This returns the opcode to reboot an instance, and its decorator
634
  will wrap this into a loop rebooting all desired instances.
635

636
  @param name: the name of the instance to act on
637
  @param opts: the command line options selected by the user
638
  @return: the opcode needed for the operation
639

640
  """
641
  return opcodes.OpInstanceReboot(instance_name=name,
642
                                  reboot_type=opts.reboot_type,
643
                                  ignore_secondaries=opts.ignore_secondaries,
644
                                  shutdown_timeout=opts.shutdown_timeout)
645

    
646

    
647
def _ShutdownInstance(name, opts):
648
  """Shutdown an instance.
649

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

653
  @param name: the name of the instance to act on
654
  @param opts: the command line options selected by the user
655
  @return: the opcode needed for the operation
656

657
  """
658
  return opcodes.OpInstanceShutdown(instance_name=name,
659
                                    force=opts.force,
660
                                    timeout=opts.timeout,
661
                                    ignore_offline_nodes=opts.ignore_offline,
662
                                    no_remember=opts.no_remember)
663

    
664

    
665
def ReplaceDisks(opts, args):
666
  """Replace the disks of an instance
667

668
  @param opts: the command line options selected by the user
669
  @type args: list
670
  @param args: should contain only one element, the instance name
671
  @rtype: int
672
  @return: the desired exit code
673

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

    
703
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
704
                                      remote_node=new_2ndary, mode=mode,
705
                                      iallocator=iallocator,
706
                                      early_release=opts.early_release,
707
                                      ignore_ipolicy=opts.ignore_ipolicy)
708
  SubmitOrSend(op, opts)
709
  return 0
710

    
711

    
712
def FailoverInstance(opts, args):
713
  """Failover an instance.
714

715
  The failover is done by shutting it down on its present node and
716
  starting it on the secondary.
717

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

724
  """
725
  cl = GetClient()
726
  instance_name = args[0]
727
  force = opts.force
728
  iallocator = opts.iallocator
729
  target_node = opts.dst_node
730

    
731
  if iallocator and target_node:
732
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
733
                               " node (-n) but not both", errors.ECODE_INVAL)
734

    
735
  if not force:
736
    _EnsureInstancesExist(cl, [instance_name])
737

    
738
    usertext = ("Failover will happen to image %s."
739
                " This requires a shutdown of the instance. Continue?" %
740
                (instance_name,))
741
    if not AskUser(usertext):
742
      return 1
743

    
744
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
745
                                  ignore_consistency=opts.ignore_consistency,
746
                                  shutdown_timeout=opts.shutdown_timeout,
747
                                  iallocator=iallocator,
748
                                  target_node=target_node,
749
                                  ignore_ipolicy=opts.ignore_ipolicy)
750
  SubmitOrSend(op, opts, cl=cl)
751
  return 0
752

    
753

    
754
def MigrateInstance(opts, args):
755
  """Migrate an instance.
756

757
  The migrate is done without shutdown.
758

759
  @param opts: the command line options selected by the user
760
  @type args: list
761
  @param args: should contain only one element, the instance name
762
  @rtype: int
763
  @return: the desired exit code
764

765
  """
766
  cl = GetClient()
767
  instance_name = args[0]
768
  force = opts.force
769
  iallocator = opts.iallocator
770
  target_node = opts.dst_node
771

    
772
  if iallocator and target_node:
773
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
774
                               " node (-n) but not both", errors.ECODE_INVAL)
775

    
776
  if not force:
777
    _EnsureInstancesExist(cl, [instance_name])
778

    
779
    if opts.cleanup:
780
      usertext = ("Instance %s will be recovered from a failed migration."
781
                  " Note that the migration procedure (including cleanup)" %
782
                  (instance_name,))
783
    else:
784
      usertext = ("Instance %s will be migrated. Note that migration" %
785
                  (instance_name,))
786
    usertext += (" might impact the instance if anything goes wrong"
787
                 " (e.g. due to bugs in the hypervisor). Continue?")
788
    if not AskUser(usertext):
789
      return 1
790

    
791
  # this should be removed once --non-live is deprecated
792
  if not opts.live and opts.migration_mode is not None:
793
    raise errors.OpPrereqError("Only one of the --non-live and "
794
                               "--migration-mode options can be passed",
795
                               errors.ECODE_INVAL)
796
  if not opts.live: # --non-live passed
797
    mode = constants.HT_MIGRATION_NONLIVE
798
  else:
799
    mode = opts.migration_mode
800

    
801
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
802
                                 cleanup=opts.cleanup, iallocator=iallocator,
803
                                 target_node=target_node,
804
                                 allow_failover=opts.allow_failover,
805
                                 allow_runtime_changes=opts.allow_runtime_chgs,
806
                                 ignore_ipolicy=opts.ignore_ipolicy)
807
  SubmitOrSend(op, cl=cl, opts=opts)
808
  return 0
809

    
810

    
811
def MoveInstance(opts, args):
812
  """Move an instance.
813

814
  @param opts: the command line options selected by the user
815
  @type args: list
816
  @param args: should contain only one element, the instance name
817
  @rtype: int
818
  @return: the desired exit code
819

820
  """
821
  cl = GetClient()
822
  instance_name = args[0]
823
  force = opts.force
824

    
825
  if not force:
826
    usertext = ("Instance %s will be moved."
827
                " This requires a shutdown of the instance. Continue?" %
828
                (instance_name,))
829
    if not AskUser(usertext):
830
      return 1
831

    
832
  op = opcodes.OpInstanceMove(instance_name=instance_name,
833
                              target_node=opts.node,
834
                              compress=opts.compress,
835
                              shutdown_timeout=opts.shutdown_timeout,
836
                              ignore_consistency=opts.ignore_consistency,
837
                              ignore_ipolicy=opts.ignore_ipolicy)
838
  SubmitOrSend(op, opts, cl=cl)
839
  return 0
840

    
841

    
842
def ConnectToInstanceConsole(opts, args):
843
  """Connect to the console of an instance.
844

845
  @param opts: the command line options selected by the user
846
  @type args: list
847
  @param args: should contain only one element, the instance name
848
  @rtype: int
849
  @return: the desired exit code
850

851
  """
852
  instance_name = args[0]
853

    
854
  cl = GetClient()
855
  qcl = GetClient(query=True)
856
  try:
857
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
858
    ((console_data, oper_state), ) = \
859
      qcl.QueryInstances([instance_name], ["console", "oper_state"], False)
860
  finally:
861
    # Ensure client connection is closed while external commands are run
862
    cl.Close()
863
    qcl.Close()
864

    
865
  del cl
866
  del qcl
867

    
868
  if not console_data:
869
    if oper_state:
870
      # Instance is running
871
      raise errors.OpExecError("Console information for instance %s is"
872
                               " unavailable" % instance_name)
873
    else:
874
      raise errors.OpExecError("Instance %s is not running, can't get console" %
875
                               instance_name)
876

    
877
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
878
                    opts.show_command, cluster_name)
879

    
880

    
881
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
882
               _runcmd_fn=utils.RunCmd):
883
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
884

885
  @type console: L{objects.InstanceConsole}
886
  @param console: Console object
887
  @type show_command: bool
888
  @param show_command: Whether to just display commands
889
  @type cluster_name: string
890
  @param cluster_name: Cluster name as retrieved from master daemon
891

892
  """
893
  assert console.Validate()
894

    
895
  if console.kind == constants.CONS_MESSAGE:
896
    feedback_fn(console.message)
897
  elif console.kind == constants.CONS_VNC:
898
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
899
                " URL <vnc://%s:%s/>",
900
                console.instance, console.host, console.port,
901
                console.display, console.host, console.port)
902
  elif console.kind == constants.CONS_SPICE:
903
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
904
                console.host, console.port)
905
  elif console.kind == constants.CONS_SSH:
906
    # Convert to string if not already one
907
    if isinstance(console.command, basestring):
908
      cmd = console.command
909
    else:
910
      cmd = utils.ShellQuoteArgs(console.command)
911

    
912
    srun = ssh.SshRunner(cluster_name=cluster_name)
913
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
914
                            port=console.port,
915
                            batch=True, quiet=False, tty=True)
916

    
917
    if show_command:
918
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
919
    else:
920
      result = _runcmd_fn(ssh_cmd, interactive=True)
921
      if result.failed:
922
        logging.error("Console command \"%s\" failed with reason '%s' and"
923
                      " output %r", result.cmd, result.fail_reason,
924
                      result.output)
925
        raise errors.OpExecError("Connection to console of instance %s failed,"
926
                                 " please check cluster configuration" %
927
                                 console.instance)
928
  else:
929
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
930

    
931
  return constants.EXIT_SUCCESS
932

    
933

    
934
def _FormatDiskDetails(dev_type, dev, roman):
935
  """Formats the logical_id of a disk.
936

937
  """
938
  if dev_type == constants.DT_DRBD8:
939
    drbd_info = dev["drbd_info"]
940
    data = [
941
      ("nodeA", "%s, minor=%s" %
942
                (drbd_info["primary_node"],
943
                 compat.TryToRoman(drbd_info["primary_minor"],
944
                                   convert=roman))),
945
      ("nodeB", "%s, minor=%s" %
946
                (drbd_info["secondary_node"],
947
                 compat.TryToRoman(drbd_info["secondary_minor"],
948
                                   convert=roman))),
949
      ("port", str(compat.TryToRoman(drbd_info["port"], convert=roman))),
950
      ("auth key", str(drbd_info["secret"])),
951
      ]
952
  elif dev_type == constants.DT_PLAIN:
953
    vg_name, lv_name = dev["logical_id"]
954
    data = ["%s/%s" % (vg_name, lv_name)]
955
  else:
956
    data = [str(dev["logical_id"])]
957

    
958
  return data
959

    
960

    
961
def _FormatBlockDevInfo(idx, top_level, dev, roman):
962
  """Show block device information.
963

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

967
  @type idx: int
968
  @param idx: the index of the current disk
969
  @type top_level: boolean
970
  @param top_level: if this a top-level disk?
971
  @type dev: dict
972
  @param dev: dictionary with disk information
973
  @type roman: boolean
974
  @param roman: whether to try to use roman integers
975
  @return: a list of either strings, tuples or lists
976
      (which should be formatted at a higher indent level)
977

978
  """
979
  def helper(dtype, status):
980
    """Format one line for physical device status.
981

982
    @type dtype: str
983
    @param dtype: a constant from the L{constants.DTS_BLOCK} set
984
    @type status: tuple
985
    @param status: a tuple as returned from L{backend.FindBlockDevice}
986
    @return: the string representing the status
987

988
    """
989
    if not status:
990
      return "not active"
991
    txt = ""
992
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
993
    if major is None:
994
      major_string = "N/A"
995
    else:
996
      major_string = str(compat.TryToRoman(major, convert=roman))
997

    
998
    if minor is None:
999
      minor_string = "N/A"
1000
    else:
1001
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1002

    
1003
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
1004
    if dtype in (constants.DT_DRBD8, ):
1005
      if syncp is not None:
1006
        sync_text = "*RECOVERING* %5.2f%%," % syncp
1007
        if estt:
1008
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1009
        else:
1010
          sync_text += " ETA unknown"
1011
      else:
1012
        sync_text = "in sync"
1013
      if degr:
1014
        degr_text = "*DEGRADED*"
1015
      else:
1016
        degr_text = "ok"
1017
      if ldisk_status == constants.LDS_FAULTY:
1018
        ldisk_text = " *MISSING DISK*"
1019
      elif ldisk_status == constants.LDS_UNKNOWN:
1020
        ldisk_text = " *UNCERTAIN STATE*"
1021
      else:
1022
        ldisk_text = ""
1023
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1024
    elif dtype == constants.DT_PLAIN:
1025
      if ldisk_status == constants.LDS_FAULTY:
1026
        ldisk_text = " *FAILED* (failed drive?)"
1027
      else:
1028
        ldisk_text = ""
1029
      txt += ldisk_text
1030
    return txt
1031

    
1032
  # the header
1033
  if top_level:
1034
    if dev["iv_name"] is not None:
1035
      txt = dev["iv_name"]
1036
    else:
1037
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1038
  else:
1039
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1040
  if isinstance(dev["size"], int):
1041
    nice_size = utils.FormatUnit(dev["size"], "h")
1042
  else:
1043
    nice_size = str(dev["size"])
1044
  data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1045
  if top_level:
1046
    if dev["spindles"] is not None:
1047
      data.append(("spindles", dev["spindles"]))
1048
    data.append(("access mode", dev["mode"]))
1049
  if dev["logical_id"] is not None:
1050
    try:
1051
      l_id = _FormatDiskDetails(dev["dev_type"], dev, roman)
1052
    except ValueError:
1053
      l_id = [str(dev["logical_id"])]
1054
    if len(l_id) == 1:
1055
      data.append(("logical_id", l_id[0]))
1056
    else:
1057
      data.extend(l_id)
1058

    
1059
  if dev["pstatus"]:
1060
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1061

    
1062
  if dev["sstatus"]:
1063
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1064

    
1065
  data.append(("name", dev["name"]))
1066
  data.append(("UUID", dev["uuid"]))
1067

    
1068
  if dev["children"]:
1069
    data.append(("child devices", [
1070
      _FormatBlockDevInfo(c_idx, False, child, roman)
1071
      for c_idx, child in enumerate(dev["children"])
1072
      ]))
1073
  return data
1074

    
1075

    
1076
def _FormatInstanceNicInfo(idx, nic):
1077
  """Helper function for L{_FormatInstanceInfo()}"""
1078
  (name, uuid, ip, mac, mode, link, vlan, _, netinfo) = nic
1079
  network_name = None
1080
  if netinfo:
1081
    network_name = netinfo["name"]
1082
  return [
1083
    ("nic/%d" % idx, ""),
1084
    ("MAC", str(mac)),
1085
    ("IP", str(ip)),
1086
    ("mode", str(mode)),
1087
    ("link", str(link)),
1088
    ("vlan", str(vlan)),
1089
    ("network", str(network_name)),
1090
    ("UUID", str(uuid)),
1091
    ("name", str(name)),
1092
    ]
1093

    
1094

    
1095
def _FormatInstanceNodesInfo(instance):
1096
  """Helper function for L{_FormatInstanceInfo()}"""
1097
  pgroup = ("%s (UUID %s)" %
1098
            (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1099
  secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1100
                          (name, group_name, group_uuid))
1101
                         for (name, group_name, group_uuid) in
1102
                           zip(instance["snodes"],
1103
                               instance["snodes_group_names"],
1104
                               instance["snodes_group_uuids"]))
1105
  return [
1106
    [
1107
      ("primary", instance["pnode"]),
1108
      ("group", pgroup),
1109
      ],
1110
    [("secondaries", secs)],
1111
    ]
1112

    
1113

    
1114
def _GetVncConsoleInfo(instance):
1115
  """Helper function for L{_FormatInstanceInfo()}"""
1116
  vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1117
                                               None)
1118
  if vnc_bind_address:
1119
    port = instance["network_port"]
1120
    display = int(port) - constants.VNC_BASE_PORT
1121
    if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1122
      vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1123
                                                 port,
1124
                                                 display)
1125
    elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1126
      vnc_console_port = ("%s:%s (node %s) (display %s)" %
1127
                           (vnc_bind_address, port,
1128
                            instance["pnode"], display))
1129
    else:
1130
      # vnc bind address is a file
1131
      vnc_console_port = "%s:%s" % (instance["pnode"],
1132
                                    vnc_bind_address)
1133
    ret = "vnc to %s" % vnc_console_port
1134
  else:
1135
    ret = None
1136
  return ret
1137

    
1138

    
1139
def _FormatInstanceInfo(instance, roman_integers):
1140
  """Format instance information for L{cli.PrintGenericInfo()}"""
1141
  istate = "configured to be %s" % instance["config_state"]
1142
  if instance["run_state"]:
1143
    istate += ", actual state is %s" % instance["run_state"]
1144
  info = [
1145
    ("Instance name", instance["name"]),
1146
    ("UUID", instance["uuid"]),
1147
    ("Serial number",
1148
     str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1149
    ("Creation time", utils.FormatTime(instance["ctime"])),
1150
    ("Modification time", utils.FormatTime(instance["mtime"])),
1151
    ("State", istate),
1152
    ("Nodes", _FormatInstanceNodesInfo(instance)),
1153
    ("Operating system", instance["os"]),
1154
    ("Operating system parameters",
1155
     FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1156
    ]
1157

    
1158
  if "network_port" in instance:
1159
    info.append(("Allocated network port",
1160
                 str(compat.TryToRoman(instance["network_port"],
1161
                                       convert=roman_integers))))
1162
  info.append(("Hypervisor", instance["hypervisor"]))
1163
  console = _GetVncConsoleInfo(instance)
1164
  if console:
1165
    info.append(("console connection", console))
1166
  # deprecated "memory" value, kept for one version for compatibility
1167
  # TODO(ganeti 2.7) remove.
1168
  be_actual = copy.deepcopy(instance["be_actual"])
1169
  be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1170
  info.extend([
1171
    ("Hypervisor parameters",
1172
     FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1173
    ("Back-end parameters",
1174
     FormatParamsDictInfo(instance["be_instance"], be_actual)),
1175
    ("NICs", [
1176
      _FormatInstanceNicInfo(idx, nic)
1177
      for (idx, nic) in enumerate(instance["nics"])
1178
      ]),
1179
    ("Disk template", instance["disk_template"]),
1180
    ("Disks", [
1181
      _FormatBlockDevInfo(idx, True, device, roman_integers)
1182
      for (idx, device) in enumerate(instance["disks"])
1183
      ]),
1184
    ])
1185
  return info
1186

    
1187

    
1188
def ShowInstanceConfig(opts, args):
1189
  """Compute instance run-time status.
1190

1191
  @param opts: the command line options selected by the user
1192
  @type args: list
1193
  @param args: either an empty list, and then we query all
1194
      instances, or should contain a list of instance names
1195
  @rtype: int
1196
  @return: the desired exit code
1197

1198
  """
1199
  if not args and not opts.show_all:
1200
    ToStderr("No instance selected."
1201
             " Please pass in --all if you want to query all instances.\n"
1202
             "Note that this can take a long time on a big cluster.")
1203
    return 1
1204
  elif args and opts.show_all:
1205
    ToStderr("Cannot use --all if you specify instance names.")
1206
    return 1
1207

    
1208
  retcode = 0
1209
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1210
                                   use_locking=not opts.static)
1211
  result = SubmitOpCode(op, opts=opts)
1212
  if not result:
1213
    ToStdout("No instances.")
1214
    return 1
1215

    
1216
  PrintGenericInfo([
1217
    _FormatInstanceInfo(instance, opts.roman_integers)
1218
    for instance in result.values()
1219
    ])
1220
  return retcode
1221

    
1222

    
1223
def _ConvertNicDiskModifications(mods):
1224
  """Converts NIC/disk modifications from CLI to opcode.
1225

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

1231
  @type mods: list of tuples
1232
  @param mods: Modifications as given by command line parser
1233
  @rtype: list of tuples
1234
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1235

1236
  """
1237
  result = []
1238

    
1239
  for (identifier, params) in mods:
1240
    if identifier == constants.DDM_ADD:
1241
      # Add item as last item (legacy interface)
1242
      action = constants.DDM_ADD
1243
      identifier = -1
1244
    elif identifier == constants.DDM_REMOVE:
1245
      # Remove last item (legacy interface)
1246
      action = constants.DDM_REMOVE
1247
      identifier = -1
1248
    else:
1249
      # Modifications and adding/removing at arbitrary indices
1250
      add = params.pop(constants.DDM_ADD, _MISSING)
1251
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1252
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1253

    
1254
      if modify is _MISSING:
1255
        if not (add is _MISSING or remove is _MISSING):
1256
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1257
                                     errors.ECODE_INVAL)
1258
        elif add is not _MISSING:
1259
          action = constants.DDM_ADD
1260
        elif remove is not _MISSING:
1261
          action = constants.DDM_REMOVE
1262
        else:
1263
          action = constants.DDM_MODIFY
1264

    
1265
      elif add is _MISSING and remove is _MISSING:
1266
        action = constants.DDM_MODIFY
1267
      else:
1268
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1269
                                   " same time", errors.ECODE_INVAL)
1270

    
1271
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1272

    
1273
    if action == constants.DDM_REMOVE and params:
1274
      raise errors.OpPrereqError("Not accepting parameters on removal",
1275
                                 errors.ECODE_INVAL)
1276

    
1277
    result.append((action, identifier, params))
1278

    
1279
  return result
1280

    
1281

    
1282
def _ParseDiskSizes(mods):
1283
  """Parses disk sizes in parameters.
1284

1285
  """
1286
  for (action, _, params) in mods:
1287
    if params and constants.IDISK_SIZE in params:
1288
      params[constants.IDISK_SIZE] = \
1289
        utils.ParseUnit(params[constants.IDISK_SIZE])
1290
    elif action == constants.DDM_ADD:
1291
      raise errors.OpPrereqError("Missing required parameter 'size'",
1292
                                 errors.ECODE_INVAL)
1293

    
1294
  return mods
1295

    
1296

    
1297
def SetInstanceParams(opts, args):
1298
  """Modifies an instance.
1299

1300
  All parameters take effect only at the next restart of the instance.
1301

1302
  @param opts: the command line options selected by the user
1303
  @type args: list
1304
  @param args: should contain only one element, the instance name
1305
  @rtype: int
1306
  @return: the desired exit code
1307

1308
  """
1309
  if not (opts.nics or opts.disks or opts.disk_template or
1310
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1311
          opts.offline_inst or opts.online_inst or opts.runtime_mem or
1312
          opts.new_primary_node):
1313
    ToStderr("Please give at least one of the parameters.")
1314
    return 1
1315

    
1316
  for param in opts.beparams:
1317
    if isinstance(opts.beparams[param], basestring):
1318
      if opts.beparams[param].lower() == "default":
1319
        opts.beparams[param] = constants.VALUE_DEFAULT
1320

    
1321
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1322
                      allowed_values=[constants.VALUE_DEFAULT])
1323

    
1324
  for param in opts.hvparams:
1325
    if isinstance(opts.hvparams[param], basestring):
1326
      if opts.hvparams[param].lower() == "default":
1327
        opts.hvparams[param] = constants.VALUE_DEFAULT
1328

    
1329
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1330
                      allowed_values=[constants.VALUE_DEFAULT])
1331

    
1332
  nics = _ConvertNicDiskModifications(opts.nics)
1333
  for action, _, __ in nics:
1334
    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
1335
      usertext = ("You are about to hot-modify a NIC. This will be done"
1336
                  " by removing the exisiting and then adding a new one."
1337
                  " Network connection might be lost. Continue?")
1338
      if not AskUser(usertext):
1339
        return 1
1340

    
1341
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1342

    
1343
  if (opts.disk_template and
1344
      opts.disk_template in constants.DTS_INT_MIRROR and
1345
      not opts.node):
1346
    ToStderr("Changing the disk template to a mirrored one requires"
1347
             " specifying a secondary node")
1348
    return 1
1349

    
1350
  if opts.offline_inst:
1351
    offline = True
1352
  elif opts.online_inst:
1353
    offline = False
1354
  else:
1355
    offline = None
1356

    
1357
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1358
                                   nics=nics,
1359
                                   disks=disks,
1360
                                   hotplug=opts.hotplug,
1361
                                   disk_template=opts.disk_template,
1362
                                   remote_node=opts.node,
1363
                                   pnode=opts.new_primary_node,
1364
                                   hvparams=opts.hvparams,
1365
                                   beparams=opts.beparams,
1366
                                   runtime_mem=opts.runtime_mem,
1367
                                   os_name=opts.os,
1368
                                   osparams=opts.osparams,
1369
                                   force_variant=opts.force_variant,
1370
                                   force=opts.force,
1371
                                   wait_for_sync=opts.wait_for_sync,
1372
                                   offline=offline,
1373
                                   conflicts_check=opts.conflicts_check,
1374
                                   ignore_ipolicy=opts.ignore_ipolicy)
1375

    
1376
  # even if here we process the result, we allow submit only
1377
  result = SubmitOrSend(op, opts)
1378

    
1379
  if result:
1380
    ToStdout("Modified instance %s", args[0])
1381
    for param, data in result:
1382
      ToStdout(" - %-5s -> %s", param, data)
1383
    ToStdout("Please don't forget that most parameters take effect"
1384
             " only at the next (re)start of the instance initiated by"
1385
             " ganeti; restarting from within the instance will"
1386
             " not be enough.")
1387
  return 0
1388

    
1389

    
1390
def ChangeGroup(opts, args):
1391
  """Moves an instance to another group.
1392

1393
  """
1394
  (instance_name, ) = args
1395

    
1396
  cl = GetClient()
1397

    
1398
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1399
                                     iallocator=opts.iallocator,
1400
                                     target_groups=opts.to,
1401
                                     early_release=opts.early_release)
1402
  result = SubmitOrSend(op, opts, cl=cl)
1403

    
1404
  # Keep track of submitted jobs
1405
  jex = JobExecutor(cl=cl, opts=opts)
1406

    
1407
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1408
    jex.AddJobId(None, status, job_id)
1409

    
1410
  results = jex.GetResults()
1411
  bad_cnt = len([row for row in results if not row[0]])
1412
  if bad_cnt == 0:
1413
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1414
    rcode = constants.EXIT_SUCCESS
1415
  else:
1416
    ToStdout("There were %s errors while changing group of instance '%s'.",
1417
             bad_cnt, instance_name)
1418
    rcode = constants.EXIT_FAILURE
1419

    
1420
  return rcode
1421

    
1422

    
1423
# multi-instance selection options
1424
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1425
                           help="Do not ask for confirmation when more than"
1426
                           " one instance is affected",
1427
                           action="store_true", default=False)
1428

    
1429
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1430
                            help="Filter by nodes (primary only)",
1431
                            const=_EXPAND_NODES_PRI, action="store_const")
1432

    
1433
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1434
                            help="Filter by nodes (secondary only)",
1435
                            const=_EXPAND_NODES_SEC, action="store_const")
1436

    
1437
m_node_opt = cli_option("--node", dest="multi_mode",
1438
                        help="Filter by nodes (primary and secondary)",
1439
                        const=_EXPAND_NODES_BOTH, action="store_const")
1440

    
1441
m_clust_opt = cli_option("--all", dest="multi_mode",
1442
                         help="Select all instances in the cluster",
1443
                         const=_EXPAND_CLUSTER, action="store_const")
1444

    
1445
m_inst_opt = cli_option("--instance", dest="multi_mode",
1446
                        help="Filter by instance name [default]",
1447
                        const=_EXPAND_INSTANCES, action="store_const")
1448

    
1449
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1450
                             help="Filter by node tag",
1451
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1452
                             action="store_const")
1453

    
1454
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1455
                                 help="Filter by primary node tag",
1456
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1457
                                 action="store_const")
1458

    
1459
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1460
                                 help="Filter by secondary node tag",
1461
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1462
                                 action="store_const")
1463

    
1464
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1465
                             help="Filter by instance tag",
1466
                             const=_EXPAND_INSTANCES_BY_TAGS,
1467
                             action="store_const")
1468

    
1469
# this is defined separately due to readability only
1470
add_opts = [
1471
  NOSTART_OPT,
1472
  OS_OPT,
1473
  FORCE_VARIANT_OPT,
1474
  NO_INSTALL_OPT,
1475
  IGNORE_IPOLICY_OPT,
1476
  ]
1477

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

    
1631
#: dictionary with aliases for commands
1632
aliases = {
1633
  "start": "startup",
1634
  "stop": "shutdown",
1635
  "show": "info",
1636
  }
1637

    
1638

    
1639
def Main():
1640
  return GenericMain(commands, aliases=aliases,
1641
                     override={"tag_type": constants.TAG_INSTANCE},
1642
                     env_override=_ENV_OVERRIDE)