Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 4c6e8e1a

History | View | Annotate | Download (56.1 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 RemoveInstance(opts, args):
407
  """Remove 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
412
      the instance to be removed
413
  @rtype: int
414
  @return: the desired exit code
415

416
  """
417
  instance_name = args[0]
418
  force = opts.force
419
  cl = GetClient()
420

    
421
  if not force:
422
    _EnsureInstancesExist(cl, [instance_name])
423

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

    
430
  op = opcodes.OpInstanceRemove(instance_name=instance_name,
431
                                ignore_failures=opts.ignore_failures,
432
                                shutdown_timeout=opts.shutdown_timeout)
433
  SubmitOrSend(op, opts, cl=cl)
434
  return 0
435

    
436

    
437
def RenameInstance(opts, args):
438
  """Rename an instance.
439

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

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

    
453
  op = opcodes.OpInstanceRename(instance_name=args[0],
454
                                new_name=args[1],
455
                                ip_check=opts.ip_check,
456
                                name_check=opts.name_check)
457
  result = SubmitOrSend(op, opts)
458

    
459
  if result:
460
    ToStdout("Instance '%s' renamed to '%s'", args[0], result)
461

    
462
  return 0
463

    
464

    
465
def ActivateDisks(opts, args):
466
  """Activate an instance's disks.
467

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

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

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

    
489

    
490
def DeactivateDisks(opts, args):
491
  """Deactivate an instance's disks.
492

493
  This function takes the instance name, looks for its primary node
494
  and the tries to shutdown its block devices on that node.
495

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

502
  """
503
  instance_name = args[0]
504
  op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name,
505
                                         force=opts.force)
506
  SubmitOrSend(op, opts)
507
  return 0
508

    
509

    
510
def RecreateDisks(opts, args):
511
  """Recreate an instance's disks.
512

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

519
  """
520
  instance_name = args[0]
521

    
522
  disks = []
523

    
524
  if opts.disks:
525
    for didx, ddict in opts.disks:
526
      didx = int(didx)
527

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

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

    
540
      disks.append((didx, ddict))
541

    
542
    # TODO: Verify modifyable parameters (already done in
543
    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
544

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

    
556
  op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
557
                                       disks=disks, nodes=nodes,
558
                                       iallocator=opts.iallocator)
559
  SubmitOrSend(op, opts)
560

    
561
  return 0
562

    
563

    
564
def GrowDisk(opts, args):
565
  """Grow an instance's disks.
566

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

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

    
594

    
595
def _StartupInstance(name, opts):
596
  """Startup instances.
597

598
  This returns the opcode to start an instance, and its decorator will
599
  wrap this into a loop starting all desired instances.
600

601
  @param name: the name of the instance to act on
602
  @param opts: the command line options selected by the user
603
  @return: the opcode needed for the operation
604

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

    
618

    
619
def _RebootInstance(name, opts):
620
  """Reboot instance(s).
621

622
  This returns the opcode to reboot an instance, and its decorator
623
  will wrap this into a loop rebooting all desired instances.
624

625
  @param name: the name of the instance to act on
626
  @param opts: the command line options selected by the user
627
  @return: the opcode needed for the operation
628

629
  """
630
  return opcodes.OpInstanceReboot(instance_name=name,
631
                                  reboot_type=opts.reboot_type,
632
                                  ignore_secondaries=opts.ignore_secondaries,
633
                                  shutdown_timeout=opts.shutdown_timeout)
634

    
635

    
636
def _ShutdownInstance(name, opts):
637
  """Shutdown an instance.
638

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

642
  @param name: the name of the instance to act on
643
  @param opts: the command line options selected by the user
644
  @return: the opcode needed for the operation
645

646
  """
647
  return opcodes.OpInstanceShutdown(instance_name=name,
648
                                    force=opts.force,
649
                                    timeout=opts.timeout,
650
                                    ignore_offline_nodes=opts.ignore_offline,
651
                                    no_remember=opts.no_remember)
652

    
653

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

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

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

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

    
700

    
701
def FailoverInstance(opts, args):
702
  """Failover an instance.
703

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

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

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

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

    
724
  if not force:
725
    _EnsureInstancesExist(cl, [instance_name])
726

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

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

    
742

    
743
def MigrateInstance(opts, args):
744
  """Migrate an instance.
745

746
  The migrate is done without shutdown.
747

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

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

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

    
765
  if not force:
766
    _EnsureInstancesExist(cl, [instance_name])
767

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

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

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

    
799

    
800
def MoveInstance(opts, args):
801
  """Move an instance.
802

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

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

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

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

    
829

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

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

839
  """
840
  instance_name = args[0]
841

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

    
851
  del cl
852

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

    
862
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
863
                    opts.show_command, cluster_name)
864

    
865

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

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

877
  """
878
  assert console.Validate()
879

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

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

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

    
915
  return constants.EXIT_SUCCESS
916

    
917

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

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

    
938
  return data
939

    
940

    
941
def _FormatListInfo(data):
942
  return list(str(i) for i in data)
943

    
944

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

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

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

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

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

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

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

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

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

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

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

    
1049
  data.append(("name", dev["name"]))
1050
  data.append(("UUID", dev["uuid"]))
1051

    
1052
  if dev["children"]:
1053
    data.append(("child devices", [
1054
      _FormatBlockDevInfo(c_idx, False, child, roman)
1055
      for c_idx, child in enumerate(dev["children"])
1056
      ]))
1057
  return data
1058

    
1059

    
1060
def _FormatInstanceNicInfo(idx, nic):
1061
  """Helper function for L{_FormatInstanceInfo()}"""
1062
  (name, uuid, ip, mac, mode, link, _, netinfo) = nic
1063
  network_name = None
1064
  if netinfo:
1065
    network_name = netinfo["name"]
1066
  return [
1067
    ("nic/%d" % idx, ""),
1068
    ("MAC", str(mac)),
1069
    ("IP", str(ip)),
1070
    ("mode", str(mode)),
1071
    ("link", str(link)),
1072
    ("network", str(network_name)),
1073
    ("UUID", str(uuid)),
1074
    ("name", str(name)),
1075
    ]
1076

    
1077

    
1078
def _FormatInstanceNodesInfo(instance):
1079
  """Helper function for L{_FormatInstanceInfo()}"""
1080
  pgroup = ("%s (UUID %s)" %
1081
            (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1082
  secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
1083
                          (name, group_name, group_uuid))
1084
                         for (name, group_name, group_uuid) in
1085
                           zip(instance["snodes"],
1086
                               instance["snodes_group_names"],
1087
                               instance["snodes_group_uuids"]))
1088
  return [
1089
    [
1090
      ("primary", instance["pnode"]),
1091
      ("group", pgroup),
1092
      ],
1093
    [("secondaries", secs)],
1094
    ]
1095

    
1096

    
1097
def _GetVncConsoleInfo(instance):
1098
  """Helper function for L{_FormatInstanceInfo()}"""
1099
  vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1100
                                               None)
1101
  if vnc_bind_address:
1102
    port = instance["network_port"]
1103
    display = int(port) - constants.VNC_BASE_PORT
1104
    if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1105
      vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1106
                                                 port,
1107
                                                 display)
1108
    elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1109
      vnc_console_port = ("%s:%s (node %s) (display %s)" %
1110
                           (vnc_bind_address, port,
1111
                            instance["pnode"], display))
1112
    else:
1113
      # vnc bind address is a file
1114
      vnc_console_port = "%s:%s" % (instance["pnode"],
1115
                                    vnc_bind_address)
1116
    ret = "vnc to %s" % vnc_console_port
1117
  else:
1118
    ret = None
1119
  return ret
1120

    
1121

    
1122
def _FormatInstanceInfo(instance, roman_integers):
1123
  """Format instance information for L{cli.PrintGenericInfo()}"""
1124
  istate = "configured to be %s" % instance["config_state"]
1125
  if instance["run_state"]:
1126
    istate += ", actual state is %s" % instance["run_state"]
1127
  info = [
1128
    ("Instance name", instance["name"]),
1129
    ("UUID", instance["uuid"]),
1130
    ("Serial number",
1131
     str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
1132
    ("Creation time", utils.FormatTime(instance["ctime"])),
1133
    ("Modification time", utils.FormatTime(instance["mtime"])),
1134
    ("State", istate),
1135
    ("Nodes", _FormatInstanceNodesInfo(instance)),
1136
    ("Operating system", instance["os"]),
1137
    ("Operating system parameters",
1138
     FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
1139
    ]
1140

    
1141
  if "network_port" in instance:
1142
    info.append(("Allocated network port",
1143
                 str(compat.TryToRoman(instance["network_port"],
1144
                                       convert=roman_integers))))
1145
  info.append(("Hypervisor", instance["hypervisor"]))
1146
  console = _GetVncConsoleInfo(instance)
1147
  if console:
1148
    info.append(("console connection", console))
1149
  # deprecated "memory" value, kept for one version for compatibility
1150
  # TODO(ganeti 2.7) remove.
1151
  be_actual = copy.deepcopy(instance["be_actual"])
1152
  be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1153
  info.extend([
1154
    ("Hypervisor parameters",
1155
     FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
1156
    ("Back-end parameters",
1157
     FormatParamsDictInfo(instance["be_instance"], be_actual)),
1158
    ("NICs", [
1159
      _FormatInstanceNicInfo(idx, nic)
1160
      for (idx, nic) in enumerate(instance["nics"])
1161
      ]),
1162
    ("Disk template", instance["disk_template"]),
1163
    ("Disks", [
1164
      _FormatBlockDevInfo(idx, True, device, roman_integers)
1165
      for (idx, device) in enumerate(instance["disks"])
1166
      ]),
1167
    ])
1168
  return info
1169

    
1170

    
1171
def ShowInstanceConfig(opts, args):
1172
  """Compute instance run-time status.
1173

1174
  @param opts: the command line options selected by the user
1175
  @type args: list
1176
  @param args: either an empty list, and then we query all
1177
      instances, or should contain a list of instance names
1178
  @rtype: int
1179
  @return: the desired exit code
1180

1181
  """
1182
  if not args and not opts.show_all:
1183
    ToStderr("No instance selected."
1184
             " Please pass in --all if you want to query all instances.\n"
1185
             "Note that this can take a long time on a big cluster.")
1186
    return 1
1187
  elif args and opts.show_all:
1188
    ToStderr("Cannot use --all if you specify instance names.")
1189
    return 1
1190

    
1191
  retcode = 0
1192
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1193
                                   use_locking=not opts.static)
1194
  result = SubmitOpCode(op, opts=opts)
1195
  if not result:
1196
    ToStdout("No instances.")
1197
    return 1
1198

    
1199
  PrintGenericInfo([
1200
    _FormatInstanceInfo(instance, opts.roman_integers)
1201
    for instance in result.values()
1202
    ])
1203
  return retcode
1204

    
1205

    
1206
def _ConvertNicDiskModifications(mods):
1207
  """Converts NIC/disk modifications from CLI to opcode.
1208

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

1214
  @type mods: list of tuples
1215
  @param mods: Modifications as given by command line parser
1216
  @rtype: list of tuples
1217
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1218

1219
  """
1220
  result = []
1221

    
1222
  for (identifier, params) in mods:
1223
    if identifier == constants.DDM_ADD:
1224
      # Add item as last item (legacy interface)
1225
      action = constants.DDM_ADD
1226
      identifier = -1
1227
    elif identifier == constants.DDM_REMOVE:
1228
      # Remove last item (legacy interface)
1229
      action = constants.DDM_REMOVE
1230
      identifier = -1
1231
    else:
1232
      # Modifications and adding/removing at arbitrary indices
1233
      add = params.pop(constants.DDM_ADD, _MISSING)
1234
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1235
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1236

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

    
1248
      elif add is _MISSING and remove is _MISSING:
1249
        action = constants.DDM_MODIFY
1250
      else:
1251
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1252
                                   " same time", errors.ECODE_INVAL)
1253

    
1254
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1255

    
1256
    if action == constants.DDM_REMOVE and params:
1257
      raise errors.OpPrereqError("Not accepting parameters on removal",
1258
                                 errors.ECODE_INVAL)
1259

    
1260
    result.append((action, identifier, params))
1261

    
1262
  return result
1263

    
1264

    
1265
def _ParseDiskSizes(mods):
1266
  """Parses disk sizes in parameters.
1267

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

    
1277
  return mods
1278

    
1279

    
1280
def SetInstanceParams(opts, args):
1281
  """Modifies an instance.
1282

1283
  All parameters take effect only at the next restart of the instance.
1284

1285
  @param opts: the command line options selected by the user
1286
  @type args: list
1287
  @param args: should contain only one element, the instance name
1288
  @rtype: int
1289
  @return: the desired exit code
1290

1291
  """
1292
  if not (opts.nics or opts.disks or opts.disk_template or
1293
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1294
          opts.offline_inst or opts.online_inst or opts.runtime_mem or
1295
          opts.new_primary_node):
1296
    ToStderr("Please give at least one of the parameters.")
1297
    return 1
1298

    
1299
  for param in opts.beparams:
1300
    if isinstance(opts.beparams[param], basestring):
1301
      if opts.beparams[param].lower() == "default":
1302
        opts.beparams[param] = constants.VALUE_DEFAULT
1303

    
1304
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1305
                      allowed_values=[constants.VALUE_DEFAULT])
1306

    
1307
  for param in opts.hvparams:
1308
    if isinstance(opts.hvparams[param], basestring):
1309
      if opts.hvparams[param].lower() == "default":
1310
        opts.hvparams[param] = constants.VALUE_DEFAULT
1311

    
1312
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1313
                      allowed_values=[constants.VALUE_DEFAULT])
1314

    
1315
  nics = _ConvertNicDiskModifications(opts.nics)
1316
  for action, _, __ in nics:
1317
    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
1318
      usertext = ("You are about to hot-modify a NIC. This will be done"
1319
                  " by removing the exisiting and then adding a new one."
1320
                  " Network connection might be lost. Continue?")
1321
      if not AskUser(usertext):
1322
        return 1
1323

    
1324
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1325

    
1326
  if (opts.disk_template and
1327
      opts.disk_template in constants.DTS_INT_MIRROR and
1328
      not opts.node):
1329
    ToStderr("Changing the disk template to a mirrored one requires"
1330
             " specifying a secondary node")
1331
    return 1
1332

    
1333
  if opts.offline_inst:
1334
    offline = True
1335
  elif opts.online_inst:
1336
    offline = False
1337
  else:
1338
    offline = None
1339

    
1340
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1341
                                   nics=nics,
1342
                                   disks=disks,
1343
                                   hotplug=opts.hotplug,
1344
                                   disk_template=opts.disk_template,
1345
                                   remote_node=opts.node,
1346
                                   pnode=opts.new_primary_node,
1347
                                   hvparams=opts.hvparams,
1348
                                   beparams=opts.beparams,
1349
                                   runtime_mem=opts.runtime_mem,
1350
                                   os_name=opts.os,
1351
                                   osparams=opts.osparams,
1352
                                   force_variant=opts.force_variant,
1353
                                   force=opts.force,
1354
                                   wait_for_sync=opts.wait_for_sync,
1355
                                   offline=offline,
1356
                                   conflicts_check=opts.conflicts_check,
1357
                                   ignore_ipolicy=opts.ignore_ipolicy)
1358

    
1359
  # even if here we process the result, we allow submit only
1360
  result = SubmitOrSend(op, opts)
1361

    
1362
  if result:
1363
    ToStdout("Modified instance %s", args[0])
1364
    for param, data in result:
1365
      ToStdout(" - %-5s -> %s", param, data)
1366
    ToStdout("Please don't forget that most parameters take effect"
1367
             " only at the next (re)start of the instance initiated by"
1368
             " ganeti; restarting from within the instance will"
1369
             " not be enough.")
1370
  return 0
1371

    
1372

    
1373
def ChangeGroup(opts, args):
1374
  """Moves an instance to another group.
1375

1376
  """
1377
  (instance_name, ) = args
1378

    
1379
  cl = GetClient()
1380

    
1381
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1382
                                     iallocator=opts.iallocator,
1383
                                     target_groups=opts.to,
1384
                                     early_release=opts.early_release)
1385
  result = SubmitOrSend(op, opts, cl=cl)
1386

    
1387
  # Keep track of submitted jobs
1388
  jex = JobExecutor(cl=cl, opts=opts)
1389

    
1390
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1391
    jex.AddJobId(None, status, job_id)
1392

    
1393
  results = jex.GetResults()
1394
  bad_cnt = len([row for row in results if not row[0]])
1395
  if bad_cnt == 0:
1396
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1397
    rcode = constants.EXIT_SUCCESS
1398
  else:
1399
    ToStdout("There were %s errors while changing group of instance '%s'.",
1400
             bad_cnt, instance_name)
1401
    rcode = constants.EXIT_FAILURE
1402

    
1403
  return rcode
1404

    
1405

    
1406
# multi-instance selection options
1407
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1408
                           help="Do not ask for confirmation when more than"
1409
                           " one instance is affected",
1410
                           action="store_true", default=False)
1411

    
1412
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1413
                            help="Filter by nodes (primary only)",
1414
                            const=_EXPAND_NODES_PRI, action="store_const")
1415

    
1416
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1417
                            help="Filter by nodes (secondary only)",
1418
                            const=_EXPAND_NODES_SEC, action="store_const")
1419

    
1420
m_node_opt = cli_option("--node", dest="multi_mode",
1421
                        help="Filter by nodes (primary and secondary)",
1422
                        const=_EXPAND_NODES_BOTH, action="store_const")
1423

    
1424
m_clust_opt = cli_option("--all", dest="multi_mode",
1425
                         help="Select all instances in the cluster",
1426
                         const=_EXPAND_CLUSTER, action="store_const")
1427

    
1428
m_inst_opt = cli_option("--instance", dest="multi_mode",
1429
                        help="Filter by instance name [default]",
1430
                        const=_EXPAND_INSTANCES, action="store_const")
1431

    
1432
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1433
                             help="Filter by node tag",
1434
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1435
                             action="store_const")
1436

    
1437
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1438
                                 help="Filter by primary node tag",
1439
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1440
                                 action="store_const")
1441

    
1442
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1443
                                 help="Filter by secondary node tag",
1444
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1445
                                 action="store_const")
1446

    
1447
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1448
                             help="Filter by instance tag",
1449
                             const=_EXPAND_INSTANCES_BY_TAGS,
1450
                             action="store_const")
1451

    
1452
# this is defined separately due to readability only
1453
add_opts = [
1454
  NOSTART_OPT,
1455
  OS_OPT,
1456
  FORCE_VARIANT_OPT,
1457
  NO_INSTALL_OPT,
1458
  IGNORE_IPOLICY_OPT,
1459
  ]
1460

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

    
1607
#: dictionary with aliases for commands
1608
aliases = {
1609
  "start": "startup",
1610
  "stop": "shutdown",
1611
  "show": "info",
1612
  }
1613

    
1614

    
1615
def Main():
1616
  return GenericMain(commands, aliases=aliases,
1617
                     override={"tag_type": constants.TAG_INSTANCE},
1618
                     env_override=_ENV_OVERRIDE)