Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (57.3 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
    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
  qcl = GetClient(query=True)
424

    
425
  if not force:
426
    _EnsureInstancesExist(qcl, [instance_name])
427

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

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

    
440

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

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

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

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

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

    
466
  return 0
467

    
468

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

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

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

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

    
493

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

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

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

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

    
513

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

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

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

    
526
  disks = []
527

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

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

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

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

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

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

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

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

    
573
  return 0
574

    
575

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

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

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

    
606

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

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

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

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

    
630

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

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

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

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

    
647

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

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

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

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

    
665

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

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

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

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

    
712

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

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

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

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

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

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

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

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

    
754

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

758
  The migrate is done without shutdown.
759

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

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

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

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

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

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

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

    
811

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

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

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

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

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

    
842

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

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

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

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

    
866
  del cl
867
  del qcl
868

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

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

    
881

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

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

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

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

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

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

    
932
  return constants.EXIT_SUCCESS
933

    
934

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

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

    
959
  return data
960

    
961

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1076

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

    
1095

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

    
1114

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

    
1139

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

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

    
1188

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

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

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

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

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

    
1223

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

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

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

1237
  """
1238
  result = []
1239

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

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

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

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

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

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

    
1280
  return result
1281

    
1282

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

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

    
1298
  return mods
1299

    
1300

    
1301
def SetInstanceParams(opts, args):
1302
  """Modifies an instance.
1303

1304
  All parameters take effect only at the next restart of the instance.
1305

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

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

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

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

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

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

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

    
1346
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1347

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

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

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

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

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

    
1396

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

1400
  """
1401
  (instance_name, ) = args
1402

    
1403
  cl = GetClient()
1404

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

    
1411
  # Keep track of submitted jobs
1412
  jex = JobExecutor(cl=cl, opts=opts)
1413

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

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

    
1427
  return rcode
1428

    
1429

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

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

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

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

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

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

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

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

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

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

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

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

    
1641
#: dictionary with aliases for commands
1642
aliases = {
1643
  "start": "startup",
1644
  "stop": "shutdown",
1645
  "show": "info",
1646
  }
1647

    
1648

    
1649
def Main():
1650
  return GenericMain(commands, aliases=aliases,
1651
                     override={"tag_type": constants.TAG_INSTANCE},
1652
                     env_override=_ENV_OVERRIDE)