Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ d027b72b

History | View | Annotate | Download (57.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2014 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 L{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
                                     osparams_private=opts.osparams_private,
400
                                     osparams_secret=opts.osparams_secret)
401
    jex.QueueJob(instance_name, op)
402

    
403
  results = jex.WaitOrShow(not opts.submit_only)
404

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

    
410

    
411
def RemoveInstance(opts, args):
412
  """Remove an instance.
413

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

421
  """
422
  instance_name = args[0]
423
  force = opts.force
424
  cl = GetClient()
425
  qcl = GetClient(query=True)
426

    
427
  if not force:
428
    _EnsureInstancesExist(qcl, [instance_name])
429

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

    
436
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
437
                                ignore_failures=opts.ignore_failures,
438
                                shutdown_timeout=opts.shutdown_timeout)
439
  SubmitOrSend(op, opts, cl=cl)
440
  return 0
441

    
442

    
443
def RenameInstance(opts, args):
444
  """Rename an instance.
445

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

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

    
459
  op = opcodes.OpInstanceRename(instance_name=args[0],
460
                                new_name=args[1],
461
                                ip_check=opts.ip_check,
462
                                name_check=opts.name_check)
463
  result = SubmitOrSend(op, opts)
464

    
465
  if result:
466
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
467

    
468
  return 0
469

    
470

    
471
def ActivateDisks(opts, args):
472
  """Activate an instance's disks.
473

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

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

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

    
495

    
496
def DeactivateDisks(opts, args):
497
  """Deactivate an instance's disks.
498

499
  This function takes the instance name, looks for its primary node
500
  and the tries to shutdown its block devices on that node.
501

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

508
  """
509
  instance_name = args[0]
510
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
511
                                         force=opts.force)
512
  SubmitOrSend(op, opts)
513
  return 0
514

    
515

    
516
def RecreateDisks(opts, args):
517
  """Recreate an instance's disks.
518

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

525
  """
526
  instance_name = args[0]
527

    
528
  disks = []
529

    
530
  if opts.disks:
531
    for didx, ddict in opts.disks:
532
      didx = int(didx)
533

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

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

    
546
      if constants.IDISK_SPINDLES in ddict:
547
        try:
548
          ddict[constants.IDISK_SPINDLES] = \
549
              int(ddict[constants.IDISK_SPINDLES])
550
        except ValueError, err:
551
          raise errors.OpPrereqError("Invalid spindles for disk %d: %s" %
552
                                     (didx, err), errors.ECODE_INVAL)
553

    
554
      disks.append((didx, ddict))
555

    
556
    # TODO: Verify modifyable parameters (already done in
557
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
558

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

    
570
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
571
                                       disks=disks, nodes=nodes,
572
                                       iallocator=opts.iallocator)
573
  SubmitOrSend(op, opts)
574

    
575
  return 0
576

    
577

    
578
def GrowDisk(opts, args):
579
  """Grow an instance's disks.
580

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

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

    
608

    
609
def _StartupInstance(name, opts):
610
  """Startup instances.
611

612
  This returns the opcode to start an instance, and its decorator will
613
  wrap this into a loop starting all desired instances.
614

615
  @param name: the name of the instance to act on
616
  @param opts: the command line options selected by the user
617
  @return: the opcode needed for the operation
618

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

    
632

    
633
def _RebootInstance(name, opts):
634
  """Reboot instance(s).
635

636
  This returns the opcode to reboot an instance, and its decorator
637
  will wrap this into a loop rebooting all desired instances.
638

639
  @param name: the name of the instance to act on
640
  @param opts: the command line options selected by the user
641
  @return: the opcode needed for the operation
642

643
  """
644
  return opcodes.OpInstanceReboot(instance_name=name,
645
                                  reboot_type=opts.reboot_type,
646
                                  ignore_secondaries=opts.ignore_secondaries,
647
                                  shutdown_timeout=opts.shutdown_timeout)
648

    
649

    
650
def _ShutdownInstance(name, opts):
651
  """Shutdown an instance.
652

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

656
  @param name: the name of the instance to act on
657
  @param opts: the command line options selected by the user
658
  @return: the opcode needed for the operation
659

660
  """
661
  return opcodes.OpInstanceShutdown(instance_name=name,
662
                                    force=opts.force,
663
                                    timeout=opts.timeout,
664
                                    ignore_offline_nodes=opts.ignore_offline,
665
                                    no_remember=opts.no_remember)
666

    
667

    
668
def ReplaceDisks(opts, args):
669
  """Replace the disks of an instance
670

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

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

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

    
714

    
715
def FailoverInstance(opts, args):
716
  """Failover an instance.
717

718
  The failover is done by shutting it down on its present node and
719
  starting it on the secondary.
720

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

727
  """
728
  cl = GetClient()
729
  instance_name = args[0]
730
  force = opts.force
731
  iallocator = opts.iallocator
732
  target_node = opts.dst_node
733

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

    
738
  if not force:
739
    _EnsureInstancesExist(cl, [instance_name])
740

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

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

    
756

    
757
def MigrateInstance(opts, args):
758
  """Migrate an instance.
759

760
  The migrate is done without shutdown.
761

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

768
  """
769
  cl = GetClient()
770
  instance_name = args[0]
771
  force = opts.force
772
  iallocator = opts.iallocator
773
  target_node = opts.dst_node
774

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

    
779
  if not force:
780
    _EnsureInstancesExist(cl, [instance_name])
781

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

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

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

    
813

    
814
def MoveInstance(opts, args):
815
  """Move an instance.
816

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

823
  """
824
  cl = GetClient()
825
  instance_name = args[0]
826
  force = opts.force
827

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

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

    
844

    
845
def ConnectToInstanceConsole(opts, args):
846
  """Connect to the console of an instance.
847

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

854
  """
855
  instance_name = args[0]
856

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

    
868
  del cl
869
  del qcl
870

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

    
880
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
881
                    opts.show_command, cluster_name)
882

    
883

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

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

895
  """
896
  assert console.Validate()
897

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

    
915
    srun = ssh.SshRunner(cluster_name=cluster_name)
916
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
917
                            port=console.port,
918
                            batch=True, quiet=False, tty=True)
919

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

    
934
  return constants.EXIT_SUCCESS
935

    
936

    
937
def _FormatDiskDetails(dev_type, dev, roman):
938
  """Formats the logical_id of a disk.
939

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

    
961
  return data
962

    
963

    
964
def _FormatBlockDevInfo(idx, top_level, dev, roman):
965
  """Show block device information.
966

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

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

981
  """
982
  def helper(dtype, status):
983
    """Format one line for physical device status.
984

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

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

    
1001
    if minor is None:
1002
      minor_string = "N/A"
1003
    else:
1004
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1005

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

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

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

    
1065
  if dev["sstatus"]:
1066
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1067

    
1068
  data.append(("name", dev["name"]))
1069
  data.append(("UUID", dev["uuid"]))
1070

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

    
1078

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

    
1097

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

    
1116

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

    
1141

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

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

    
1190

    
1191
def ShowInstanceConfig(opts, args):
1192
  """Compute instance run-time status.
1193

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

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

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

    
1219
  PrintGenericInfo([
1220
    _FormatInstanceInfo(instance, opts.roman_integers)
1221
    for instance in result.values()
1222
    ])
1223
  return retcode
1224

    
1225

    
1226
def _ConvertNicDiskModifications(mods):
1227
  """Converts NIC/disk modifications from CLI to opcode.
1228

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

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

1239
  """
1240
  result = []
1241

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

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

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

    
1274
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1275

    
1276
    if action == constants.DDM_REMOVE and params:
1277
      raise errors.OpPrereqError("Not accepting parameters on removal",
1278
                                 errors.ECODE_INVAL)
1279

    
1280
    result.append((action, identifier, params))
1281

    
1282
  return result
1283

    
1284

    
1285
def _ParseDiskSizes(mods):
1286
  """Parses disk sizes in parameters.
1287

1288
  """
1289
  for (action, _, params) in mods:
1290
    if params and constants.IDISK_SPINDLES in params:
1291
      params[constants.IDISK_SPINDLES] = \
1292
          int(params[constants.IDISK_SPINDLES])
1293
    if params and constants.IDISK_SIZE in params:
1294
      params[constants.IDISK_SIZE] = \
1295
        utils.ParseUnit(params[constants.IDISK_SIZE])
1296
    elif action == constants.DDM_ADD:
1297
      raise errors.OpPrereqError("Missing required parameter 'size'",
1298
                                 errors.ECODE_INVAL)
1299

    
1300
  return mods
1301

    
1302

    
1303
def SetInstanceParams(opts, args):
1304
  """Modifies an instance.
1305

1306
  All parameters take effect only at the next restart of the instance.
1307

1308
  @param opts: the command line options selected by the user
1309
  @type args: list
1310
  @param args: should contain only one element, the instance name
1311
  @rtype: int
1312
  @return: the desired exit code
1313

1314
  """
1315
  if not (opts.nics or opts.disks or opts.disk_template or opts.hvparams or
1316
          opts.beparams or opts.os or opts.osparams or opts.osparams_private
1317
          or opts.offline_inst or opts.online_inst or opts.runtime_mem or
1318
          opts.new_primary_node):
1319
    ToStderr("Please give at least one of the parameters.")
1320
    return 1
1321

    
1322
  for param in opts.beparams:
1323
    if isinstance(opts.beparams[param], basestring):
1324
      if opts.beparams[param].lower() == "default":
1325
        opts.beparams[param] = constants.VALUE_DEFAULT
1326

    
1327
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1328
                      allowed_values=[constants.VALUE_DEFAULT])
1329

    
1330
  for param in opts.hvparams:
1331
    if isinstance(opts.hvparams[param], basestring):
1332
      if opts.hvparams[param].lower() == "default":
1333
        opts.hvparams[param] = constants.VALUE_DEFAULT
1334

    
1335
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1336
                      allowed_values=[constants.VALUE_DEFAULT])
1337
  FixHvParams(opts.hvparams)
1338

    
1339
  nics = _ConvertNicDiskModifications(opts.nics)
1340
  for action, _, __ in nics:
1341
    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
1342
      usertext = ("You are about to hot-modify a NIC. This will be done"
1343
                  " by removing the exisiting and then adding a new one."
1344
                  " Network connection might be lost. Continue?")
1345
      if not AskUser(usertext):
1346
        return 1
1347

    
1348
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1349

    
1350
  if (opts.disk_template and
1351
      opts.disk_template in constants.DTS_INT_MIRROR and
1352
      not opts.node):
1353
    ToStderr("Changing the disk template to a mirrored one requires"
1354
             " specifying a secondary node")
1355
    return 1
1356

    
1357
  if opts.offline_inst:
1358
    offline = True
1359
  elif opts.online_inst:
1360
    offline = False
1361
  else:
1362
    offline = None
1363

    
1364
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1365
                                   nics=nics,
1366
                                   disks=disks,
1367
                                   hotplug=opts.hotplug,
1368
                                   hotplug_if_possible=opts.hotplug_if_possible,
1369
                                   disk_template=opts.disk_template,
1370
                                   remote_node=opts.node,
1371
                                   pnode=opts.new_primary_node,
1372
                                   hvparams=opts.hvparams,
1373
                                   beparams=opts.beparams,
1374
                                   runtime_mem=opts.runtime_mem,
1375
                                   os_name=opts.os,
1376
                                   osparams=opts.osparams,
1377
                                   osparams_private=opts.osparams_private,
1378
                                   force_variant=opts.force_variant,
1379
                                   force=opts.force,
1380
                                   wait_for_sync=opts.wait_for_sync,
1381
                                   offline=offline,
1382
                                   conflicts_check=opts.conflicts_check,
1383
                                   ignore_ipolicy=opts.ignore_ipolicy)
1384

    
1385
  # even if here we process the result, we allow submit only
1386
  result = SubmitOrSend(op, opts)
1387

    
1388
  if result:
1389
    ToStdout("Modified instance %s", args[0])
1390
    for param, data in result:
1391
      ToStdout(" - %-5s -> %s", param, data)
1392
    ToStdout("Please don't forget that most parameters take effect"
1393
             " only at the next (re)start of the instance initiated by"
1394
             " ganeti; restarting from within the instance will"
1395
             " not be enough.")
1396
  return 0
1397

    
1398

    
1399
def ChangeGroup(opts, args):
1400
  """Moves an instance to another group.
1401

1402
  """
1403
  (instance_name, ) = args
1404

    
1405
  cl = GetClient()
1406

    
1407
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1408
                                     iallocator=opts.iallocator,
1409
                                     target_groups=opts.to,
1410
                                     early_release=opts.early_release)
1411
  result = SubmitOrSend(op, opts, cl=cl)
1412

    
1413
  # Keep track of submitted jobs
1414
  jex = JobExecutor(cl=cl, opts=opts)
1415

    
1416
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1417
    jex.AddJobId(None, status, job_id)
1418

    
1419
  results = jex.GetResults()
1420
  bad_cnt = len([row for row in results if not row[0]])
1421
  if bad_cnt == 0:
1422
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1423
    rcode = constants.EXIT_SUCCESS
1424
  else:
1425
    ToStdout("There were %s errors while changing group of instance '%s'.",
1426
             bad_cnt, instance_name)
1427
    rcode = constants.EXIT_FAILURE
1428

    
1429
  return rcode
1430

    
1431

    
1432
# multi-instance selection options
1433
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1434
                           help="Do not ask for confirmation when more than"
1435
                           " one instance is affected",
1436
                           action="store_true", default=False)
1437

    
1438
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1439
                            help="Filter by nodes (primary only)",
1440
                            const=_EXPAND_NODES_PRI, action="store_const")
1441

    
1442
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1443
                            help="Filter by nodes (secondary only)",
1444
                            const=_EXPAND_NODES_SEC, action="store_const")
1445

    
1446
m_node_opt = cli_option("--node", dest="multi_mode",
1447
                        help="Filter by nodes (primary and secondary)",
1448
                        const=_EXPAND_NODES_BOTH, action="store_const")
1449

    
1450
m_clust_opt = cli_option("--all", dest="multi_mode",
1451
                         help="Select all instances in the cluster",
1452
                         const=_EXPAND_CLUSTER, action="store_const")
1453

    
1454
m_inst_opt = cli_option("--instance", dest="multi_mode",
1455
                        help="Filter by instance name [default]",
1456
                        const=_EXPAND_INSTANCES, action="store_const")
1457

    
1458
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1459
                             help="Filter by node tag",
1460
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1461
                             action="store_const")
1462

    
1463
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1464
                                 help="Filter by primary node tag",
1465
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1466
                                 action="store_const")
1467

    
1468
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1469
                                 help="Filter by secondary node tag",
1470
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1471
                                 action="store_const")
1472

    
1473
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1474
                             help="Filter by instance tag",
1475
                             const=_EXPAND_INSTANCES_BY_TAGS,
1476
                             action="store_const")
1477

    
1478
# this is defined separately due to readability only
1479
add_opts = [
1480
  NOSTART_OPT,
1481
  OS_OPT,
1482
  FORCE_VARIANT_OPT,
1483
  NO_INSTALL_OPT,
1484
  IGNORE_IPOLICY_OPT,
1485
  INSTANCE_COMMUNICATION_OPT,
1486
  ]
1487

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

    
1644
#: dictionary with aliases for commands
1645
aliases = {
1646
  "start": "startup",
1647
  "stop": "shutdown",
1648
  "show": "info",
1649
  }
1650

    
1651

    
1652
def Main():
1653
  return GenericMain(commands, aliases=aliases,
1654
                     override={"tag_type": constants.TAG_INSTANCE},
1655
                     env_override=_ENV_OVERRIDE)