Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ d0b60d3a

History | View | Annotate | Download (57.5 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

    
28
import copy
29
import itertools
30
import simplejson
31
import logging
32

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

    
44

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

    
55
_EXPAND_NODES_TAGS_MODES = compat.UniqueFrozenset([
56
  _EXPAND_NODES_BOTH_BY_TAGS,
57
  _EXPAND_NODES_PRI_BY_TAGS,
58
  _EXPAND_NODES_SEC_BY_TAGS,
59
  ])
60

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

    
66
_MISSING = object()
67
_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
68

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

    
71

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

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

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

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

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

    
205

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

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

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

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

    
226
  return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
227
                     opts.separator, not opts.no_headers,
228
                     format_override=fmtoverride, verbose=opts.verbose,
229
                     force_filter=opts.force_filter)
230

    
231

    
232
def ListInstanceFields(opts, args):
233
  """List instance fields.
234

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

241
  """
242
  return GenericListFields(constants.QR_INSTANCE, args, opts.separator,
243
                           not opts.no_headers)
244

    
245

    
246
def AddInstance(opts, args):
247
  """Add an instance to the cluster.
248

249
  This is just a wrapper over GenericInstanceCreate.
250

251
  """
252
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
253

    
254

    
255
def BatchCreate(opts, args):
256
  """Create instances using a definition file.
257

258
  This function reads a json file with L{opcodes.OpInstanceCreate}
259
  serialisations.
260

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

267
  """
268
  (json_filename,) = args
269
  cl = GetClient()
270

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

    
277
  if not _INST_DATA_VAL(instance_data):
278
    ToStderr("The instance definition file is not %s" % _INST_DATA_VAL)
279
    return 1
280

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

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

    
292
    op = opcodes.OpInstanceCreate(**inst) # pylint: disable=W0142
293
    op.Validate(False)
294
    instances.append(op)
295

    
296
  op = opcodes.OpInstanceMultiAlloc(iallocator=opts.iallocator,
297
                                    instances=instances)
298
  result = SubmitOrSend(op, opts, cl=cl)
299

    
300
  # Keep track of submitted jobs
301
  jex = JobExecutor(cl=cl, opts=opts)
302

    
303
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
304
    jex.AddJobId(None, status, job_id)
305

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

    
315
  return rcode
316

    
317

    
318
def ReinstallInstance(opts, args):
319
  """Reinstall an instance.
320

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

328
  """
329
  # first, compute the desired name list
330
  if opts.multi_mode is None:
331
    opts.multi_mode = _EXPAND_INSTANCES
332

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

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

    
343
    if not result:
344
      ToStdout("Can't get the OS list")
345
      return 1
346

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

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

    
360
    if selected == "exit":
361
      ToStderr("User aborted reinstall, exiting")
362
      return 1
363

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

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

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

    
398
  results = jex.WaitOrShow(not opts.submit_only)
399

    
400
  if compat.all(map(compat.fst, results)):
401
    return constants.EXIT_SUCCESS
402
  else:
403
    return constants.EXIT_FAILURE
404

    
405

    
406
def SnapshotInstance(opts, args):
407
  """Snapshot an instance.
408

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

416
  """
417
  instance_name = args[0]
418
  inames = _ExpandMultiNames(_EXPAND_INSTANCES, [instance_name])
419
  if not inames:
420
    raise errors.OpPrereqError("Selection filter does not match any instances",
421
                               errors.ECODE_INVAL)
422
  multi_on = len(inames) > 1
423
  jex = JobExecutor(verbose=multi_on, opts=opts)
424
  for instance_name in inames:
425
    op = opcodes.OpInstanceSnapshot(instance_name=instance_name,
426
                                    disks=opts.disks)
427
    jex.QueueJob(instance_name, op)
428

    
429
  results = jex.WaitOrShow(not opts.submit_only)
430

    
431
  if compat.all(map(compat.fst, results)):
432
    return constants.EXIT_SUCCESS
433
  else:
434
    return constants.EXIT_FAILURE
435

    
436

    
437
def RemoveInstance(opts, args):
438
  """Remove an instance.
439

440
  @param opts: the command line options selected by the user
441
  @type args: list
442
  @param args: should contain only one element, the name of
443
      the instance to be removed
444
  @rtype: int
445
  @return: the desired exit code
446

447
  """
448
  instance_name = args[0]
449
  force = opts.force
450
  cl = GetClient()
451

    
452
  if not force:
453
    _EnsureInstancesExist(cl, [instance_name])
454

    
455
    usertext = ("This will remove the volumes of the instance %s"
456
                " (including mirrors), thus removing all the data"
457
                " of the instance. Continue?") % instance_name
458
    if not AskUser(usertext):
459
      return 1
460

    
461
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
462
                                ignore_failures=opts.ignore_failures,
463
                                shutdown_timeout=opts.shutdown_timeout)
464
  SubmitOrSend(op, opts, cl=cl)
465
  return 0
466

    
467

    
468
def RenameInstance(opts, args):
469
  """Rename an instance.
470

471
  @param opts: the command line options selected by the user
472
  @type args: list
473
  @param args: should contain two elements, the old and the
474
      new instance names
475
  @rtype: int
476
  @return: the desired exit code
477

478
  """
479
  if not opts.name_check:
480
    if not AskUser("As you disabled the check of the DNS entry, please verify"
481
                   " that '%s' is a FQDN. Continue?" % args[1]):
482
      return 1
483

    
484
  op = opcodes.OpInstanceRename(instance_name=args[0],
485
                                new_name=args[1],
486
                                ip_check=opts.ip_check,
487
                                name_check=opts.name_check)
488
  result = SubmitOrSend(op, opts)
489

    
490
  if result:
491
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
492

    
493
  return 0
494

    
495

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

499
  This serves two purposes:
500
    - it allows (as long as the instance is not running)
501
      mounting the disks and modifying them from the node
502
    - it repairs inactive secondary drbds
503

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

510
  """
511
  instance_name = args[0]
512
  op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
513
                                       ignore_size=opts.ignore_size,
514
                                       wait_for_sync=opts.wait_for_sync)
515
  disks_info = SubmitOrSend(op, opts)
516
  for host, iname, nname in disks_info:
517
    ToStdout("%s:%s:%s", host, iname, nname)
518
  return 0
519

    
520

    
521
def DeactivateDisks(opts, args):
522
  """Deactivate an instance's disks.
523

524
  This function takes the instance name, looks for its primary node
525
  and the tries to shutdown its block devices on that node.
526

527
  @param opts: the command line options selected by the user
528
  @type args: list
529
  @param args: should contain only one element, the instance name
530
  @rtype: int
531
  @return: the desired exit code
532

533
  """
534
  instance_name = args[0]
535
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
536
                                         force=opts.force)
537
  SubmitOrSend(op, opts)
538
  return 0
539

    
540

    
541
def RecreateDisks(opts, args):
542
  """Recreate an instance's disks.
543

544
  @param opts: the command line options selected by the user
545
  @type args: list
546
  @param args: should contain only one element, the instance name
547
  @rtype: int
548
  @return: the desired exit code
549

550
  """
551
  instance_name = args[0]
552

    
553
  disks = []
554

    
555
  if opts.disks:
556
    for didx, ddict in opts.disks:
557
      didx = int(didx)
558

    
559
      if not ht.TDict(ddict):
560
        msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
561
        raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
562

    
563
      if constants.IDISK_SIZE in ddict:
564
        try:
565
          ddict[constants.IDISK_SIZE] = \
566
            utils.ParseUnit(ddict[constants.IDISK_SIZE])
567
        except ValueError, err:
568
          raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
569
                                     (didx, err), errors.ECODE_INVAL)
570

    
571
      disks.append((didx, ddict))
572

    
573
    # TODO: Verify modifyable parameters (already done in
574
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
575

    
576
  if opts.node:
577
    if opts.iallocator:
578
      msg = "At most one of either --nodes or --iallocator can be passed"
579
      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
580
    pnode, snode = SplitNodeOption(opts.node)
581
    nodes = [pnode]
582
    if snode is not None:
583
      nodes.append(snode)
584
  else:
585
    nodes = []
586

    
587
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
588
                                       disks=disks, nodes=nodes,
589
                                       iallocator=opts.iallocator)
590
  SubmitOrSend(op, opts)
591

    
592
  return 0
593

    
594

    
595
def GrowDisk(opts, args):
596
  """Grow an instance's disks.
597

598
  @param opts: the command line options selected by the user
599
  @type args: list
600
  @param args: should contain three elements, the target instance name,
601
      the target disk id, and the target growth
602
  @rtype: int
603
  @return: the desired exit code
604

605
  """
606
  instance = args[0]
607
  disk = args[1]
608
  try:
609
    disk = int(disk)
610
  except (TypeError, ValueError), err:
611
    raise errors.OpPrereqError("Invalid disk index: %s" % str(err),
612
                               errors.ECODE_INVAL)
613
  try:
614
    amount = utils.ParseUnit(args[2])
615
  except errors.UnitParseError:
616
    raise errors.OpPrereqError("Can't parse the given amount '%s'" % args[2],
617
                               errors.ECODE_INVAL)
618
  op = opcodes.OpInstanceGrowDisk(instance_name=instance,
619
                                  disk=disk, amount=amount,
620
                                  wait_for_sync=opts.wait_for_sync,
621
                                  absolute=opts.absolute)
622
  SubmitOrSend(op, opts)
623
  return 0
624

    
625

    
626
def _StartupInstance(name, opts):
627
  """Startup instances.
628

629
  This returns the opcode to start an instance, and its decorator will
630
  wrap this into a loop starting all desired instances.
631

632
  @param name: the name of the instance to act on
633
  @param opts: the command line options selected by the user
634
  @return: the opcode needed for the operation
635

636
  """
637
  op = opcodes.OpInstanceStartup(instance_name=name,
638
                                 force=opts.force,
639
                                 ignore_offline_nodes=opts.ignore_offline,
640
                                 no_remember=opts.no_remember,
641
                                 startup_paused=opts.startup_paused)
642
  # do not add these parameters to the opcode unless they're defined
643
  if opts.hvparams:
644
    op.hvparams = opts.hvparams
645
  if opts.beparams:
646
    op.beparams = opts.beparams
647
  return op
648

    
649

    
650
def _RebootInstance(name, opts):
651
  """Reboot instance(s).
652

653
  This returns the opcode to reboot an instance, and its decorator
654
  will wrap this into a loop rebooting 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.OpInstanceReboot(instance_name=name,
662
                                  reboot_type=opts.reboot_type,
663
                                  ignore_secondaries=opts.ignore_secondaries,
664
                                  shutdown_timeout=opts.shutdown_timeout)
665

    
666

    
667
def _ShutdownInstance(name, opts):
668
  """Shutdown an instance.
669

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

673
  @param name: the name of the instance to act on
674
  @param opts: the command line options selected by the user
675
  @return: the opcode needed for the operation
676

677
  """
678
  return opcodes.OpInstanceShutdown(instance_name=name,
679
                                    force=opts.force,
680
                                    timeout=opts.timeout,
681
                                    ignore_offline_nodes=opts.ignore_offline,
682
                                    no_remember=opts.no_remember)
683

    
684

    
685
def ReplaceDisks(opts, args):
686
  """Replace the disks of an instance
687

688
  @param opts: the command line options selected by the user
689
  @type args: list
690
  @param args: should contain only one element, the instance name
691
  @rtype: int
692
  @return: the desired exit code
693

694
  """
695
  new_2ndary = opts.dst_node
696
  iallocator = opts.iallocator
697
  if opts.disks is None:
698
    disks = []
699
  else:
700
    try:
701
      disks = [int(i) for i in opts.disks.split(",")]
702
    except (TypeError, ValueError), err:
703
      raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err),
704
                                 errors.ECODE_INVAL)
705
  cnt = [opts.on_primary, opts.on_secondary, opts.auto,
706
         new_2ndary is not None, iallocator is not None].count(True)
707
  if cnt != 1:
708
    raise errors.OpPrereqError("One and only one of the -p, -s, -a, -n and -I"
709
                               " options must be passed", errors.ECODE_INVAL)
710
  elif opts.on_primary:
711
    mode = constants.REPLACE_DISK_PRI
712
  elif opts.on_secondary:
713
    mode = constants.REPLACE_DISK_SEC
714
  elif opts.auto:
715
    mode = constants.REPLACE_DISK_AUTO
716
    if disks:
717
      raise errors.OpPrereqError("Cannot specify disks when using automatic"
718
                                 " mode", errors.ECODE_INVAL)
719
  elif new_2ndary is not None or iallocator is not None:
720
    # replace secondary
721
    mode = constants.REPLACE_DISK_CHG
722

    
723
  op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
724
                                      remote_node=new_2ndary, mode=mode,
725
                                      iallocator=iallocator,
726
                                      early_release=opts.early_release,
727
                                      ignore_ipolicy=opts.ignore_ipolicy)
728
  SubmitOrSend(op, opts)
729
  return 0
730

    
731

    
732
def FailoverInstance(opts, args):
733
  """Failover an instance.
734

735
  The failover is done by shutting it down on its present node and
736
  starting it on the secondary.
737

738
  @param opts: the command line options selected by the user
739
  @type args: list
740
  @param args: should contain only one element, the instance name
741
  @rtype: int
742
  @return: the desired exit code
743

744
  """
745
  cl = GetClient()
746
  instance_name = args[0]
747
  force = opts.force
748
  iallocator = opts.iallocator
749
  target_node = opts.dst_node
750

    
751
  if iallocator and target_node:
752
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
753
                               " node (-n) but not both", errors.ECODE_INVAL)
754

    
755
  if not force:
756
    _EnsureInstancesExist(cl, [instance_name])
757

    
758
    usertext = ("Failover will happen to image %s."
759
                " This requires a shutdown of the instance. Continue?" %
760
                (instance_name,))
761
    if not AskUser(usertext):
762
      return 1
763

    
764
  op = opcodes.OpInstanceFailover(instance_name=instance_name,
765
                                  ignore_consistency=opts.ignore_consistency,
766
                                  shutdown_timeout=opts.shutdown_timeout,
767
                                  iallocator=iallocator,
768
                                  target_node=target_node,
769
                                  ignore_ipolicy=opts.ignore_ipolicy)
770
  SubmitOrSend(op, opts, cl=cl)
771
  return 0
772

    
773

    
774
def MigrateInstance(opts, args):
775
  """Migrate an instance.
776

777
  The migrate is done without shutdown.
778

779
  @param opts: the command line options selected by the user
780
  @type args: list
781
  @param args: should contain only one element, the instance name
782
  @rtype: int
783
  @return: the desired exit code
784

785
  """
786
  cl = GetClient()
787
  instance_name = args[0]
788
  force = opts.force
789
  iallocator = opts.iallocator
790
  target_node = opts.dst_node
791

    
792
  if iallocator and target_node:
793
    raise errors.OpPrereqError("Specify either an iallocator (-I), or a target"
794
                               " node (-n) but not both", errors.ECODE_INVAL)
795

    
796
  if not force:
797
    _EnsureInstancesExist(cl, [instance_name])
798

    
799
    if opts.cleanup:
800
      usertext = ("Instance %s will be recovered from a failed migration."
801
                  " Note that the migration procedure (including cleanup)" %
802
                  (instance_name,))
803
    else:
804
      usertext = ("Instance %s will be migrated. Note that migration" %
805
                  (instance_name,))
806
    usertext += (" might impact the instance if anything goes wrong"
807
                 " (e.g. due to bugs in the hypervisor). Continue?")
808
    if not AskUser(usertext):
809
      return 1
810

    
811
  # this should be removed once --non-live is deprecated
812
  if not opts.live and opts.migration_mode is not None:
813
    raise errors.OpPrereqError("Only one of the --non-live and "
814
                               "--migration-mode options can be passed",
815
                               errors.ECODE_INVAL)
816
  if not opts.live: # --non-live passed
817
    mode = constants.HT_MIGRATION_NONLIVE
818
  else:
819
    mode = opts.migration_mode
820

    
821
  op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
822
                                 cleanup=opts.cleanup, iallocator=iallocator,
823
                                 target_node=target_node,
824
                                 allow_failover=opts.allow_failover,
825
                                 allow_runtime_changes=opts.allow_runtime_chgs,
826
                                 ignore_ipolicy=opts.ignore_ipolicy)
827
  SubmitOrSend(op, cl=cl, opts=opts)
828
  return 0
829

    
830

    
831
def MoveInstance(opts, args):
832
  """Move an instance.
833

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

840
  """
841
  cl = GetClient()
842
  instance_name = args[0]
843
  force = opts.force
844

    
845
  if not force:
846
    usertext = ("Instance %s will be moved."
847
                " This requires a shutdown of the instance. Continue?" %
848
                (instance_name,))
849
    if not AskUser(usertext):
850
      return 1
851

    
852
  op = opcodes.OpInstanceMove(instance_name=instance_name,
853
                              target_node=opts.node,
854
                              shutdown_timeout=opts.shutdown_timeout,
855
                              ignore_consistency=opts.ignore_consistency,
856
                              ignore_ipolicy=opts.ignore_ipolicy)
857
  SubmitOrSend(op, opts, cl=cl)
858
  return 0
859

    
860

    
861
def ConnectToInstanceConsole(opts, args):
862
  """Connect to the console of an instance.
863

864
  @param opts: the command line options selected by the user
865
  @type args: list
866
  @param args: should contain only one element, the instance name
867
  @rtype: int
868
  @return: the desired exit code
869

870
  """
871
  instance_name = args[0]
872

    
873
  cl = GetClient()
874
  try:
875
    cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
876
    ((console_data, oper_state), ) = \
877
      cl.QueryInstances([instance_name], ["console", "oper_state"], False)
878
  finally:
879
    # Ensure client connection is closed while external commands are run
880
    cl.Close()
881

    
882
  del cl
883

    
884
  if not console_data:
885
    if oper_state:
886
      # Instance is running
887
      raise errors.OpExecError("Console information for instance %s is"
888
                               " unavailable" % instance_name)
889
    else:
890
      raise errors.OpExecError("Instance %s is not running, can't get console" %
891
                               instance_name)
892

    
893
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
894
                    opts.show_command, cluster_name)
895

    
896

    
897
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
898
               _runcmd_fn=utils.RunCmd):
899
  """Acts based on the result of L{opcodes.OpInstanceConsole}.
900

901
  @type console: L{objects.InstanceConsole}
902
  @param console: Console object
903
  @type show_command: bool
904
  @param show_command: Whether to just display commands
905
  @type cluster_name: string
906
  @param cluster_name: Cluster name as retrieved from master daemon
907

908
  """
909
  assert console.Validate()
910

    
911
  if console.kind == constants.CONS_MESSAGE:
912
    feedback_fn(console.message)
913
  elif console.kind == constants.CONS_VNC:
914
    feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
915
                " URL <vnc://%s:%s/>",
916
                console.instance, console.host, console.port,
917
                console.display, console.host, console.port)
918
  elif console.kind == constants.CONS_SPICE:
919
    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
920
                console.host, console.port)
921
  elif console.kind == constants.CONS_SSH:
922
    # Convert to string if not already one
923
    if isinstance(console.command, basestring):
924
      cmd = console.command
925
    else:
926
      cmd = utils.ShellQuoteArgs(console.command)
927

    
928
    srun = ssh.SshRunner(cluster_name=cluster_name)
929
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
930
                            batch=True, quiet=False, tty=True)
931

    
932
    if show_command:
933
      feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
934
    else:
935
      result = _runcmd_fn(ssh_cmd, interactive=True)
936
      if result.failed:
937
        logging.error("Console command \"%s\" failed with reason '%s' and"
938
                      " output %r", result.cmd, result.fail_reason,
939
                      result.output)
940
        raise errors.OpExecError("Connection to console of instance %s failed,"
941
                                 " please check cluster configuration" %
942
                                 console.instance)
943
  else:
944
    raise errors.GenericError("Unknown console type '%s'" % console.kind)
945

    
946
  return constants.EXIT_SUCCESS
947

    
948

    
949
def _FormatDiskDetails(dev_type, dev, roman):
950
  """Formats the logical_id of a disk.
951

952
  """
953
  if dev_type == constants.DT_DRBD8:
954
    drbd_info = dev["drbd_info"]
955
    data = [
956
      ("nodeA", "%s, minor=%s" %
957
                (drbd_info["primary_node"],
958
                 compat.TryToRoman(drbd_info["primary_minor"],
959
                                   convert=roman))),
960
      ("nodeB", "%s, minor=%s" %
961
                (drbd_info["secondary_node"],
962
                 compat.TryToRoman(drbd_info["secondary_minor"],
963
                                   convert=roman))),
964
      ("port", str(compat.TryToRoman(drbd_info["port"], convert=roman))),
965
      ("auth key", str(drbd_info["secret"])),
966
      ]
967
  elif dev_type == constants.DT_PLAIN:
968
    vg_name, lv_name = dev["logical_id"]
969
    data = ["%s/%s" % (vg_name, lv_name)]
970
  else:
971
    data = [str(dev["logical_id"])]
972

    
973
  return data
974

    
975

    
976
def _FormatBlockDevInfo(idx, top_level, dev, roman):
977
  """Show block device information.
978

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

982
  @type idx: int
983
  @param idx: the index of the current disk
984
  @type top_level: boolean
985
  @param top_level: if this a top-level disk?
986
  @type dev: dict
987
  @param dev: dictionary with disk information
988
  @type roman: boolean
989
  @param roman: whether to try to use roman integers
990
  @return: a list of either strings, tuples or lists
991
      (which should be formatted at a higher indent level)
992

993
  """
994
  def helper(dtype, status):
995
    """Format one line for physical device status.
996

997
    @type dtype: str
998
    @param dtype: a constant from the L{constants.DTS_BLOCK} set
999
    @type status: tuple
1000
    @param status: a tuple as returned from L{backend.FindBlockDevice}
1001
    @return: the string representing the status
1002

1003
    """
1004
    if not status:
1005
      return "not active"
1006
    txt = ""
1007
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
1008
    if major is None:
1009
      major_string = "N/A"
1010
    else:
1011
      major_string = str(compat.TryToRoman(major, convert=roman))
1012

    
1013
    if minor is None:
1014
      minor_string = "N/A"
1015
    else:
1016
      minor_string = str(compat.TryToRoman(minor, convert=roman))
1017

    
1018
    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
1019
    if dtype in (constants.DT_DRBD8, ):
1020
      if syncp is not None:
1021
        sync_text = "*RECOVERING* %5.2f%%," % syncp
1022
        if estt:
1023
          sync_text += " ETA %ss" % compat.TryToRoman(estt, convert=roman)
1024
        else:
1025
          sync_text += " ETA unknown"
1026
      else:
1027
        sync_text = "in sync"
1028
      if degr:
1029
        degr_text = "*DEGRADED*"
1030
      else:
1031
        degr_text = "ok"
1032
      if ldisk_status == constants.LDS_FAULTY:
1033
        ldisk_text = " *MISSING DISK*"
1034
      elif ldisk_status == constants.LDS_UNKNOWN:
1035
        ldisk_text = " *UNCERTAIN STATE*"
1036
      else:
1037
        ldisk_text = ""
1038
      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
1039
    elif dtype == constants.DT_PLAIN:
1040
      if ldisk_status == constants.LDS_FAULTY:
1041
        ldisk_text = " *FAILED* (failed drive?)"
1042
      else:
1043
        ldisk_text = ""
1044
      txt += ldisk_text
1045
    return txt
1046

    
1047
  # the header
1048
  if top_level:
1049
    if dev["iv_name"] is not None:
1050
      txt = dev["iv_name"]
1051
    else:
1052
      txt = "disk %s" % compat.TryToRoman(idx, convert=roman)
1053
  else:
1054
    txt = "child %s" % compat.TryToRoman(idx, convert=roman)
1055
  if isinstance(dev["size"], int):
1056
    nice_size = utils.FormatUnit(dev["size"], "h")
1057
  else:
1058
    nice_size = str(dev["size"])
1059
  data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
1060
  if top_level:
1061
    if dev["spindles"] is not None:
1062
      data.append(("spindles", dev["spindles"]))
1063
    data.append(("access mode", dev["mode"]))
1064
  if dev["logical_id"] is not None:
1065
    try:
1066
      l_id = _FormatDiskDetails(dev["dev_type"], dev, roman)
1067
    except ValueError:
1068
      l_id = [str(dev["logical_id"])]
1069
    if len(l_id) == 1:
1070
      data.append(("logical_id", l_id[0]))
1071
    else:
1072
      data.extend(l_id)
1073

    
1074
  if dev["pstatus"]:
1075
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1076

    
1077
  if dev["sstatus"]:
1078
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1079

    
1080
  data.append(("name", dev["name"]))
1081
  data.append(("UUID", dev["uuid"]))
1082

    
1083
  if dev["children"]:
1084
    data.append(("child devices", [
1085
      _FormatBlockDevInfo(c_idx, False, child, roman)
1086
      for c_idx, child in enumerate(dev["children"])
1087
      ]))
1088
  return data
1089

    
1090

    
1091
def _FormatInstanceNicInfo(idx, nic):
1092
  """Helper function for L{_FormatInstanceInfo()}"""
1093
  (name, uuid, ip, mac, mode, link, vlan, _, netinfo) = nic
1094
  network_name = None
1095
  if netinfo:
1096
    network_name = netinfo["name"]
1097
  return [
1098
    ("nic/%d" % idx, ""),
1099
    ("MAC", str(mac)),
1100
    ("IP", str(ip)),
1101
    ("mode", str(mode)),
1102
    ("link", str(link)),
1103
    ("vlan", str(vlan)),
1104
    ("network", str(network_name)),
1105
    ("UUID", str(uuid)),
1106
    ("name", str(name)),
1107
    ]
1108

    
1109

    
1110
def _FormatInstanceNodesInfo(instance):
1111
  """Helper function for L{_FormatInstanceInfo()}"""
1112
  pgroup = ("%s (UUID %s)" %
1113
            (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1114
  secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1115
                          (name, group_name, group_uuid))
1116
                         for (name, group_name, group_uuid) in
1117
                           zip(instance["snodes"],
1118
                               instance["snodes_group_names"],
1119
                               instance["snodes_group_uuids"]))
1120
  return [
1121
    [
1122
      ("primary", instance["pnode"]),
1123
      ("group", pgroup),
1124
      ],
1125
    [("secondaries", secs)],
1126
    ]
1127

    
1128

    
1129
def _GetVncConsoleInfo(instance):
1130
  """Helper function for L{_FormatInstanceInfo()}"""
1131
  vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1132
                                               None)
1133
  if vnc_bind_address:
1134
    port = instance["network_port"]
1135
    display = int(port) - constants.VNC_BASE_PORT
1136
    if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1137
      vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1138
                                                 port,
1139
                                                 display)
1140
    elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1141
      vnc_console_port = ("%s:%s (node %s) (display %s)" %
1142
                           (vnc_bind_address, port,
1143
                            instance["pnode"], display))
1144
    else:
1145
      # vnc bind address is a file
1146
      vnc_console_port = "%s:%s" % (instance["pnode"],
1147
                                    vnc_bind_address)
1148
    ret = "vnc to %s" % vnc_console_port
1149
  else:
1150
    ret = None
1151
  return ret
1152

    
1153

    
1154
def _FormatInstanceInfo(instance, roman_integers):
1155
  """Format instance information for L{cli.PrintGenericInfo()}"""
1156
  istate = "configured to be %s" % instance["config_state"]
1157
  if instance["run_state"]:
1158
    istate += ", actual state is %s" % instance["run_state"]
1159
  info = [
1160
    ("Instance name", instance["name"]),
1161
    ("UUID", instance["uuid"]),
1162
    ("Serial number",
1163
     str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1164
    ("Creation time", utils.FormatTime(instance["ctime"])),
1165
    ("Modification time", utils.FormatTime(instance["mtime"])),
1166
    ("State", istate),
1167
    ("Nodes", _FormatInstanceNodesInfo(instance)),
1168
    ("Operating system", instance["os"]),
1169
    ("Operating system parameters",
1170
     FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1171
    ]
1172

    
1173
  if "network_port" in instance:
1174
    info.append(("Allocated network port",
1175
                 str(compat.TryToRoman(instance["network_port"],
1176
                                       convert=roman_integers))))
1177
  info.append(("Hypervisor", instance["hypervisor"]))
1178
  console = _GetVncConsoleInfo(instance)
1179
  if console:
1180
    info.append(("console connection", console))
1181
  # deprecated "memory" value, kept for one version for compatibility
1182
  # TODO(ganeti 2.7) remove.
1183
  be_actual = copy.deepcopy(instance["be_actual"])
1184
  be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1185
  info.extend([
1186
    ("Hypervisor parameters",
1187
     FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1188
    ("Back-end parameters",
1189
     FormatParamsDictInfo(instance["be_instance"], be_actual)),
1190
    ("NICs", [
1191
      _FormatInstanceNicInfo(idx, nic)
1192
      for (idx, nic) in enumerate(instance["nics"])
1193
      ]),
1194
    ("Disk template", instance["disk_template"]),
1195
    ("Disks", [
1196
      _FormatBlockDevInfo(idx, True, device, roman_integers)
1197
      for (idx, device) in enumerate(instance["disks"])
1198
      ]),
1199
    ])
1200
  return info
1201

    
1202

    
1203
def ShowInstanceConfig(opts, args):
1204
  """Compute instance run-time status.
1205

1206
  @param opts: the command line options selected by the user
1207
  @type args: list
1208
  @param args: either an empty list, and then we query all
1209
      instances, or should contain a list of instance names
1210
  @rtype: int
1211
  @return: the desired exit code
1212

1213
  """
1214
  if not args and not opts.show_all:
1215
    ToStderr("No instance selected."
1216
             " Please pass in --all if you want to query all instances.\n"
1217
             "Note that this can take a long time on a big cluster.")
1218
    return 1
1219
  elif args and opts.show_all:
1220
    ToStderr("Cannot use --all if you specify instance names.")
1221
    return 1
1222

    
1223
  retcode = 0
1224
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1225
                                   use_locking=not opts.static)
1226
  result = SubmitOpCode(op, opts=opts)
1227
  if not result:
1228
    ToStdout("No instances.")
1229
    return 1
1230

    
1231
  PrintGenericInfo([
1232
    _FormatInstanceInfo(instance, opts.roman_integers)
1233
    for instance in result.values()
1234
    ])
1235
  return retcode
1236

    
1237

    
1238
def _ConvertNicDiskModifications(mods):
1239
  """Converts NIC/disk modifications from CLI to opcode.
1240

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

1246
  @type mods: list of tuples
1247
  @param mods: Modifications as given by command line parser
1248
  @rtype: list of tuples
1249
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1250

1251
  """
1252
  result = []
1253

    
1254
  for (identifier, params) in mods:
1255
    if identifier == constants.DDM_ADD:
1256
      # Add item as last item (legacy interface)
1257
      action = constants.DDM_ADD
1258
      identifier = -1
1259
    elif identifier == constants.DDM_REMOVE:
1260
      # Remove last item (legacy interface)
1261
      action = constants.DDM_REMOVE
1262
      identifier = -1
1263
    else:
1264
      # Modifications and adding/removing at arbitrary indices
1265
      add = params.pop(constants.DDM_ADD, _MISSING)
1266
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1267
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1268

    
1269
      if modify is _MISSING:
1270
        if not (add is _MISSING or remove is _MISSING):
1271
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1272
                                     errors.ECODE_INVAL)
1273
        elif add is not _MISSING:
1274
          action = constants.DDM_ADD
1275
        elif remove is not _MISSING:
1276
          action = constants.DDM_REMOVE
1277
        else:
1278
          action = constants.DDM_MODIFY
1279

    
1280
      elif add is _MISSING and remove is _MISSING:
1281
        action = constants.DDM_MODIFY
1282
      else:
1283
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1284
                                   " same time", errors.ECODE_INVAL)
1285

    
1286
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1287

    
1288
    if action == constants.DDM_REMOVE and params:
1289
      raise errors.OpPrereqError("Not accepting parameters on removal",
1290
                                 errors.ECODE_INVAL)
1291

    
1292
    result.append((action, identifier, params))
1293

    
1294
  return result
1295

    
1296

    
1297
def _ParseDiskSizes(mods):
1298
  """Parses disk sizes in parameters.
1299

1300
  """
1301
  for (action, _, params) in mods:
1302
    if params and constants.IDISK_SIZE in params:
1303
      params[constants.IDISK_SIZE] = \
1304
        utils.ParseUnit(params[constants.IDISK_SIZE])
1305
    elif action == constants.DDM_ADD:
1306
      raise errors.OpPrereqError("Missing required parameter 'size'",
1307
                                 errors.ECODE_INVAL)
1308

    
1309
  return mods
1310

    
1311

    
1312
def SetInstanceParams(opts, args):
1313
  """Modifies an instance.
1314

1315
  All parameters take effect only at the next restart of the instance.
1316

1317
  @param opts: the command line options selected by the user
1318
  @type args: list
1319
  @param args: should contain only one element, the instance name
1320
  @rtype: int
1321
  @return: the desired exit code
1322

1323
  """
1324
  if not (opts.nics or opts.disks or opts.disk_template or
1325
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1326
          opts.offline_inst or opts.online_inst or opts.runtime_mem or
1327
          opts.new_primary_node):
1328
    ToStderr("Please give at least one of the parameters.")
1329
    return 1
1330

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

    
1336
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1337
                      allowed_values=[constants.VALUE_DEFAULT])
1338

    
1339
  for param in opts.hvparams:
1340
    if isinstance(opts.hvparams[param], basestring):
1341
      if opts.hvparams[param].lower() == "default":
1342
        opts.hvparams[param] = constants.VALUE_DEFAULT
1343

    
1344
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1345
                      allowed_values=[constants.VALUE_DEFAULT])
1346
  FixHvParams(opts.hvparams)
1347

    
1348
  nics = _ConvertNicDiskModifications(opts.nics)
1349
  for action, _, __ in nics:
1350
    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
1351
      usertext = ("You are about to hot-modify a NIC. This will be done"
1352
                  " by removing the existing NIC and then adding a new one."
1353
                  " Network connection might be lost. Continue?")
1354
      if not AskUser(usertext):
1355
        return 1
1356

    
1357
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1358

    
1359
  if (opts.disk_template and
1360
      opts.disk_template in constants.DTS_INT_MIRROR and
1361
      not opts.node):
1362
    ToStderr("Changing the disk template to a mirrored one requires"
1363
             " specifying a secondary node")
1364
    return 1
1365

    
1366
  if opts.offline_inst:
1367
    offline = True
1368
  elif opts.online_inst:
1369
    offline = False
1370
  else:
1371
    offline = None
1372

    
1373
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1374
                                   nics=nics,
1375
                                   disks=disks,
1376
                                   hotplug=opts.hotplug,
1377
                                   hotplug_if_possible=opts.hotplug_if_possible,
1378
                                   disk_template=opts.disk_template,
1379
                                   remote_node=opts.node,
1380
                                   pnode=opts.new_primary_node,
1381
                                   hvparams=opts.hvparams,
1382
                                   beparams=opts.beparams,
1383
                                   runtime_mem=opts.runtime_mem,
1384
                                   os_name=opts.os,
1385
                                   osparams=opts.osparams,
1386
                                   force_variant=opts.force_variant,
1387
                                   force=opts.force,
1388
                                   wait_for_sync=opts.wait_for_sync,
1389
                                   offline=offline,
1390
                                   conflicts_check=opts.conflicts_check,
1391
                                   ignore_ipolicy=opts.ignore_ipolicy)
1392

    
1393
  # even if here we process the result, we allow submit only
1394
  result = SubmitOrSend(op, opts)
1395

    
1396
  if result:
1397
    ToStdout("Modified instance %s", args[0])
1398
    for param, data in result:
1399
      ToStdout(" - %-5s -> %s", param, data)
1400
    ToStdout("Please don't forget that most parameters take effect"
1401
             " only at the next (re)start of the instance initiated by"
1402
             " ganeti; restarting from within the instance will"
1403
             " not be enough.")
1404
  return 0
1405

    
1406

    
1407
def ChangeGroup(opts, args):
1408
  """Moves an instance to another group.
1409

1410
  """
1411
  (instance_name, ) = args
1412

    
1413
  cl = GetClient()
1414

    
1415
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1416
                                     iallocator=opts.iallocator,
1417
                                     target_groups=opts.to,
1418
                                     early_release=opts.early_release)
1419
  result = SubmitOrSend(op, opts, cl=cl)
1420

    
1421
  # Keep track of submitted jobs
1422
  jex = JobExecutor(cl=cl, opts=opts)
1423

    
1424
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1425
    jex.AddJobId(None, status, job_id)
1426

    
1427
  results = jex.GetResults()
1428
  bad_cnt = len([row for row in results if not row[0]])
1429
  if bad_cnt == 0:
1430
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1431
    rcode = constants.EXIT_SUCCESS
1432
  else:
1433
    ToStdout("There were %s errors while changing group of instance '%s'.",
1434
             bad_cnt, instance_name)
1435
    rcode = constants.EXIT_FAILURE
1436

    
1437
  return rcode
1438

    
1439

    
1440
# multi-instance selection options
1441
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1442
                           help="Do not ask for confirmation when more than"
1443
                           " one instance is affected",
1444
                           action="store_true", default=False)
1445

    
1446
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1447
                            help="Filter by nodes (primary only)",
1448
                            const=_EXPAND_NODES_PRI, action="store_const")
1449

    
1450
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1451
                            help="Filter by nodes (secondary only)",
1452
                            const=_EXPAND_NODES_SEC, action="store_const")
1453

    
1454
m_node_opt = cli_option("--node", dest="multi_mode",
1455
                        help="Filter by nodes (primary and secondary)",
1456
                        const=_EXPAND_NODES_BOTH, action="store_const")
1457

    
1458
m_clust_opt = cli_option("--all", dest="multi_mode",
1459
                         help="Select all instances in the cluster",
1460
                         const=_EXPAND_CLUSTER, action="store_const")
1461

    
1462
m_inst_opt = cli_option("--instance", dest="multi_mode",
1463
                        help="Filter by instance name [default]",
1464
                        const=_EXPAND_INSTANCES, action="store_const")
1465

    
1466
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1467
                             help="Filter by node tag",
1468
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1469
                             action="store_const")
1470

    
1471
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1472
                                 help="Filter by primary node tag",
1473
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1474
                                 action="store_const")
1475

    
1476
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1477
                                 help="Filter by secondary node tag",
1478
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1479
                                 action="store_const")
1480

    
1481
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1482
                             help="Filter by instance tag",
1483
                             const=_EXPAND_INSTANCES_BY_TAGS,
1484
                             action="store_const")
1485

    
1486
# this is defined separately due to readability only
1487
add_opts = [
1488
  NOSTART_OPT,
1489
  OS_OPT,
1490
  FORCE_VARIANT_OPT,
1491
  NO_INSTALL_OPT,
1492
  IGNORE_IPOLICY_OPT,
1493
  ]
1494

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

    
1653
#: dictionary with aliases for commands
1654
aliases = {
1655
  "start": "startup",
1656
  "stop": "shutdown",
1657
  "show": "info",
1658
  }
1659

    
1660

    
1661
def Main():
1662
  return GenericMain(commands, aliases=aliases,
1663
                     override={"tag_type": constants.TAG_INSTANCE},
1664
                     env_override=_ENV_OVERRIDE)