Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ a9f33339

History | View | Annotate | Download (56.4 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
  cl = GetClient(query=True)
227

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

    
233

    
234
def ListInstanceFields(opts, args):
235
  """List instance fields.
236

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

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

    
247

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

251
  This is just a wrapper over GenericInstanceCreate.
252

253
  """
254
  return GenericInstanceCreate(constants.INSTANCE_CREATE, opts, args)
255

    
256

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

260
  This function reads a json file with L{opcodes.OpInstanceCreate}
261
  serialisations.
262

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

269
  """
270
  (json_filename,) = args
271
  cl = GetClient()
272

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

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

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

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

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

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

    
302
  # Keep track of submitted jobs
303
  jex = JobExecutor(cl=cl, opts=opts)
304

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

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

    
317
  return rcode
318

    
319

    
320
def ReinstallInstance(opts, args):
321
  """Reinstall an instance.
322

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

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

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

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

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

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

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

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

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

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

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

    
400
  results = jex.WaitOrShow(not opts.submit_only)
401

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

    
407

    
408
def RemoveInstance(opts, args):
409
  """Remove an instance.
410

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

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

    
423
  if not force:
424
    _EnsureInstancesExist(cl, [instance_name])
425

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

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

    
438

    
439
def RenameInstance(opts, args):
440
  """Rename an instance.
441

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

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

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

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

    
464
  return 0
465

    
466

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

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

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

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

    
491

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

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

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

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

    
511

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

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

521
  """
522
  instance_name = args[0]
523

    
524
  disks = []
525

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

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

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

    
542
      disks.append((didx, ddict))
543

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

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

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

    
563
  return 0
564

    
565

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

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

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

    
596

    
597
def _StartupInstance(name, opts):
598
  """Startup instances.
599

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

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

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

    
620

    
621
def _RebootInstance(name, opts):
622
  """Reboot instance(s).
623

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

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

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

    
637

    
638
def _ShutdownInstance(name, opts):
639
  """Shutdown an instance.
640

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

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

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

    
655

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

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

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

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

    
702

    
703
def FailoverInstance(opts, args):
704
  """Failover an instance.
705

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

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

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

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

    
726
  if not force:
727
    _EnsureInstancesExist(cl, [instance_name])
728

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

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

    
744

    
745
def MigrateInstance(opts, args):
746
  """Migrate an instance.
747

748
  The migrate is done without shutdown.
749

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

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

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

    
767
  if not force:
768
    _EnsureInstancesExist(cl, [instance_name])
769

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

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

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

    
801

    
802
def MoveInstance(opts, args):
803
  """Move an instance.
804

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

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

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

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

    
832

    
833
def ConnectToInstanceConsole(opts, args):
834
  """Connect to the console of an instance.
835

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

842
  """
843
  instance_name = args[0]
844

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

    
854
  del cl
855

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

    
865
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
866
                    opts.show_command, cluster_name)
867

    
868

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

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

880
  """
881
  assert console.Validate()
882

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

    
900
    srun = ssh.SshRunner(cluster_name=cluster_name)
901
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
902
                            port=console.port,
903
                            batch=True, quiet=False, tty=True)
904

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

    
919
  return constants.EXIT_SUCCESS
920

    
921

    
922
def _FormatDiskDetails(dev_type, dev, roman):
923
  """Formats the logical_id of a disk.
924

925
  """
926
  if dev_type == constants.DT_DRBD8:
927
    drbd_info = dev["drbd_info"]
928
    data = [
929
      ("nodeA", "%s, minor=%s" %
930
                (drbd_info["primary_node"],
931
                 compat.TryToRoman(drbd_info["primary_minor"],
932
                                   convert=roman))),
933
      ("nodeB", "%s, minor=%s" %
934
                (drbd_info["secondary_node"],
935
                 compat.TryToRoman(drbd_info["secondary_minor"],
936
                                   convert=roman))),
937
      ("port", str(compat.TryToRoman(drbd_info["port"], convert=roman))),
938
      ("auth key", str(drbd_info["secret"])),
939
      ]
940
  elif dev_type == constants.DT_PLAIN:
941
    vg_name, lv_name = dev["logical_id"]
942
    data = ["%s/%s" % (vg_name, lv_name)]
943
  else:
944
    data = [str(dev["logical_id"])]
945

    
946
  return data
947

    
948

    
949
def _FormatBlockDevInfo(idx, top_level, dev, roman):
950
  """Show block device information.
951

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

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

966
  """
967
  def helper(dtype, status):
968
    """Format one line for physical device status.
969

970
    @type dtype: str
971
    @param dtype: a constant from the L{constants.DTS_BLOCK} set
972
    @type status: tuple
973
    @param status: a tuple as returned from L{backend.FindBlockDevice}
974
    @return: the string representing the status
975

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

    
986
    if minor is None:
987
      minor_string = "N/A"
988
    else:
989
      minor_string = str(compat.TryToRoman(minor, convert=roman))
990

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

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

    
1047
  if dev["pstatus"]:
1048
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1049

    
1050
  if dev["sstatus"]:
1051
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1052

    
1053
  data.append(("name", dev["name"]))
1054
  data.append(("UUID", dev["uuid"]))
1055

    
1056
  if dev["children"]:
1057
    data.append(("child devices", [
1058
      _FormatBlockDevInfo(c_idx, False, child, roman)
1059
      for c_idx, child in enumerate(dev["children"])
1060
      ]))
1061
  return data
1062

    
1063

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

    
1082

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

    
1101

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

    
1126

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

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

    
1175

    
1176
def ShowInstanceConfig(opts, args):
1177
  """Compute instance run-time status.
1178

1179
  @param opts: the command line options selected by the user
1180
  @type args: list
1181
  @param args: either an empty list, and then we query all
1182
      instances, or should contain a list of instance names
1183
  @rtype: int
1184
  @return: the desired exit code
1185

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

    
1196
  retcode = 0
1197
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1198
                                   use_locking=not opts.static)
1199
  result = SubmitOpCode(op, opts=opts)
1200
  if not result:
1201
    ToStdout("No instances.")
1202
    return 1
1203

    
1204
  PrintGenericInfo([
1205
    _FormatInstanceInfo(instance, opts.roman_integers)
1206
    for instance in result.values()
1207
    ])
1208
  return retcode
1209

    
1210

    
1211
def _ConvertNicDiskModifications(mods):
1212
  """Converts NIC/disk modifications from CLI to opcode.
1213

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

1219
  @type mods: list of tuples
1220
  @param mods: Modifications as given by command line parser
1221
  @rtype: list of tuples
1222
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1223

1224
  """
1225
  result = []
1226

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

    
1242
      if modify is _MISSING:
1243
        if not (add is _MISSING or remove is _MISSING):
1244
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1245
                                     errors.ECODE_INVAL)
1246
        elif add is not _MISSING:
1247
          action = constants.DDM_ADD
1248
        elif remove is not _MISSING:
1249
          action = constants.DDM_REMOVE
1250
        else:
1251
          action = constants.DDM_MODIFY
1252

    
1253
      elif add is _MISSING and remove is _MISSING:
1254
        action = constants.DDM_MODIFY
1255
      else:
1256
        raise errors.OpPrereqError("Cannot modify and add/remove at the"
1257
                                   " same time", errors.ECODE_INVAL)
1258

    
1259
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1260

    
1261
    if action == constants.DDM_REMOVE and params:
1262
      raise errors.OpPrereqError("Not accepting parameters on removal",
1263
                                 errors.ECODE_INVAL)
1264

    
1265
    result.append((action, identifier, params))
1266

    
1267
  return result
1268

    
1269

    
1270
def _ParseDiskSizes(mods):
1271
  """Parses disk sizes in parameters.
1272

1273
  """
1274
  for (action, _, params) in mods:
1275
    if params and constants.IDISK_SIZE in params:
1276
      params[constants.IDISK_SIZE] = \
1277
        utils.ParseUnit(params[constants.IDISK_SIZE])
1278
    elif action == constants.DDM_ADD:
1279
      raise errors.OpPrereqError("Missing required parameter 'size'",
1280
                                 errors.ECODE_INVAL)
1281

    
1282
  return mods
1283

    
1284

    
1285
def SetInstanceParams(opts, args):
1286
  """Modifies an instance.
1287

1288
  All parameters take effect only at the next restart of the instance.
1289

1290
  @param opts: the command line options selected by the user
1291
  @type args: list
1292
  @param args: should contain only one element, the instance name
1293
  @rtype: int
1294
  @return: the desired exit code
1295

1296
  """
1297
  if not (opts.nics or opts.disks or opts.disk_template or
1298
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1299
          opts.offline_inst or opts.online_inst or opts.runtime_mem or
1300
          opts.new_primary_node):
1301
    ToStderr("Please give at least one of the parameters.")
1302
    return 1
1303

    
1304
  for param in opts.beparams:
1305
    if isinstance(opts.beparams[param], basestring):
1306
      if opts.beparams[param].lower() == "default":
1307
        opts.beparams[param] = constants.VALUE_DEFAULT
1308

    
1309
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1310
                      allowed_values=[constants.VALUE_DEFAULT])
1311

    
1312
  for param in opts.hvparams:
1313
    if isinstance(opts.hvparams[param], basestring):
1314
      if opts.hvparams[param].lower() == "default":
1315
        opts.hvparams[param] = constants.VALUE_DEFAULT
1316

    
1317
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1318
                      allowed_values=[constants.VALUE_DEFAULT])
1319

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

    
1329
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1330

    
1331
  if (opts.disk_template and
1332
      opts.disk_template in constants.DTS_INT_MIRROR and
1333
      not opts.node):
1334
    ToStderr("Changing the disk template to a mirrored one requires"
1335
             " specifying a secondary node")
1336
    return 1
1337

    
1338
  if opts.offline_inst:
1339
    offline = True
1340
  elif opts.online_inst:
1341
    offline = False
1342
  else:
1343
    offline = None
1344

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

    
1364
  # even if here we process the result, we allow submit only
1365
  result = SubmitOrSend(op, opts)
1366

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

    
1377

    
1378
def ChangeGroup(opts, args):
1379
  """Moves an instance to another group.
1380

1381
  """
1382
  (instance_name, ) = args
1383

    
1384
  cl = GetClient()
1385

    
1386
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1387
                                     iallocator=opts.iallocator,
1388
                                     target_groups=opts.to,
1389
                                     early_release=opts.early_release)
1390
  result = SubmitOrSend(op, opts, cl=cl)
1391

    
1392
  # Keep track of submitted jobs
1393
  jex = JobExecutor(cl=cl, opts=opts)
1394

    
1395
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1396
    jex.AddJobId(None, status, job_id)
1397

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

    
1408
  return rcode
1409

    
1410

    
1411
# multi-instance selection options
1412
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1413
                           help="Do not ask for confirmation when more than"
1414
                           " one instance is affected",
1415
                           action="store_true", default=False)
1416

    
1417
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1418
                            help="Filter by nodes (primary only)",
1419
                            const=_EXPAND_NODES_PRI, action="store_const")
1420

    
1421
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1422
                            help="Filter by nodes (secondary only)",
1423
                            const=_EXPAND_NODES_SEC, action="store_const")
1424

    
1425
m_node_opt = cli_option("--node", dest="multi_mode",
1426
                        help="Filter by nodes (primary and secondary)",
1427
                        const=_EXPAND_NODES_BOTH, action="store_const")
1428

    
1429
m_clust_opt = cli_option("--all", dest="multi_mode",
1430
                         help="Select all instances in the cluster",
1431
                         const=_EXPAND_CLUSTER, action="store_const")
1432

    
1433
m_inst_opt = cli_option("--instance", dest="multi_mode",
1434
                        help="Filter by instance name [default]",
1435
                        const=_EXPAND_INSTANCES, action="store_const")
1436

    
1437
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1438
                             help="Filter by node tag",
1439
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1440
                             action="store_const")
1441

    
1442
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1443
                                 help="Filter by primary node tag",
1444
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1445
                                 action="store_const")
1446

    
1447
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1448
                                 help="Filter by secondary node tag",
1449
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1450
                                 action="store_const")
1451

    
1452
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1453
                             help="Filter by instance tag",
1454
                             const=_EXPAND_INSTANCES_BY_TAGS,
1455
                             action="store_const")
1456

    
1457
# this is defined separately due to readability only
1458
add_opts = [
1459
  NOSTART_OPT,
1460
  OS_OPT,
1461
  FORCE_VARIANT_OPT,
1462
  NO_INSTALL_OPT,
1463
  IGNORE_IPOLICY_OPT,
1464
  ]
1465

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

    
1619
#: dictionary with aliases for commands
1620
aliases = {
1621
  "start": "startup",
1622
  "stop": "shutdown",
1623
  "show": "info",
1624
  }
1625

    
1626

    
1627
def Main():
1628
  return GenericMain(commands, aliases=aliases,
1629
                     override={"tag_type": constants.TAG_INSTANCE},
1630
                     env_override=_ENV_OVERRIDE)