Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 06c2fb4a

History | View | Annotate | Download (57.3 kB)

1
#
2
#
3

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

    
21
"""Instance related commands"""
22

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

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

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

    
44

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

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

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

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

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

    
71

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

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

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

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

    
97
  if client is None:
98
    client = GetClient()
99
  if mode == _EXPAND_CLUSTER:
100
    if names:
101
      raise errors.OpPrereqError("Cluster filter mode takes no arguments",
102
                                 errors.ECODE_INVAL)
103
    idata = client.QueryInstances([], ["name"], False)
104
    inames = [row[0] for row in idata]
105

    
106
  elif (mode in _EXPAND_NODES_TAGS_MODES or
107
        mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_PRI, _EXPAND_NODES_SEC)):
108
    if mode in _EXPAND_NODES_TAGS_MODES:
109
      if not names:
110
        raise errors.OpPrereqError("No node tags passed", errors.ECODE_INVAL)
111
      ndata = client.QueryNodes([], ["name", "pinst_list",
112
                                     "sinst_list", "tags"], False)
113
      ndata = [row for row in ndata if set(row[3]).intersection(names)]
114
    else:
115
      if not names:
116
        raise errors.OpPrereqError("No node names passed", errors.ECODE_INVAL)
117
      ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"],
118
                                False)
119

    
120
    ipri = [row[1] for row in ndata]
121
    pri_names = list(itertools.chain(*ipri))
122
    isec = [row[2] for row in ndata]
123
    sec_names = list(itertools.chain(*isec))
124
    if mode in (_EXPAND_NODES_BOTH, _EXPAND_NODES_BOTH_BY_TAGS):
125
      inames = pri_names + sec_names
126
    elif mode in (_EXPAND_NODES_PRI, _EXPAND_NODES_PRI_BY_TAGS):
127
      inames = pri_names
128
    elif mode in (_EXPAND_NODES_SEC, _EXPAND_NODES_SEC_BY_TAGS):
129
      inames = sec_names
130
    else:
131
      raise errors.ProgrammerError("Unhandled shutdown type")
132
  elif mode == _EXPAND_INSTANCES:
133
    if not names:
134
      raise errors.OpPrereqError("No instance names passed",
135
                                 errors.ECODE_INVAL)
136
    idata = client.QueryInstances(names, ["name"], False)
137
    inames = [row[0] for row in idata]
138
  elif mode == _EXPAND_INSTANCES_BY_TAGS:
139
    if not names:
140
      raise errors.OpPrereqError("No instance tags passed",
141
                                 errors.ECODE_INVAL)
142
    idata = client.QueryInstances([], ["name", "tags"], False)
143
    inames = [row[0] for row in idata if set(row[1]).intersection(names)]
144
  else:
145
    raise errors.OpPrereqError("Unknown mode '%s'" % mode, errors.ECODE_INVAL)
146

    
147
  return inames
148

    
149

    
150
def _EnsureInstancesExist(client, names):
151
  """Check for and ensure the given instance names exist.
152

153
  This function will raise an OpPrereqError in case they don't
154
  exist. Otherwise it will exit cleanly.
155

156
  @type client: L{ganeti.luxi.Client}
157
  @param client: the client to use for the query
158
  @type names: list
159
  @param names: the list of instance names to query
160
  @raise errors.OpPrereqError: in case any instance is missing
161

162
  """
163
  # TODO: change LUInstanceQuery to that it actually returns None
164
  # instead of raising an exception, or devise a better mechanism
165
  result = client.QueryInstances(names, ["name"], False)
166
  for orig_name, row in zip(names, result):
167
    if row[0] is None:
168
      raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name,
169
                                 errors.ECODE_NOENT)
170

    
171

    
172
def GenericManyOps(operation, fn):
173
  """Generic multi-instance operations.
174

175
  The will return a wrapper that processes the options and arguments
176
  given, and uses the passed function to build the opcode needed for
177
  the specific operation. Thus all the generic loop/confirmation code
178
  is abstracted into this function.
179

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

    
205

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

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

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

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

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

    
231

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

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

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

    
245

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

249
  This is just a wrapper over GenericInstanceCreate.
250

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

    
254

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

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

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

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

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

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

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

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

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

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

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

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

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

    
315
  return rcode
316

    
317

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
405

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

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

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

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

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

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

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

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

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

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

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

    
466

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

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

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

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

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

    
492
  return 0
493

    
494

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

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

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

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

    
519

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

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

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

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

    
539

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

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

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

    
552
  disks = []
553

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

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

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

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

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

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

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

    
591
  return 0
592

    
593

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

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

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

    
624

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

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

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

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

    
648

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

652
  This returns the opcode to reboot an instance, and its decorator
653
  will wrap this into a loop rebooting all desired instances.
654

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

659
  """
660
  return opcodes.OpInstanceReboot(instance_name=name,
661
                                  reboot_type=opts.reboot_type,
662
                                  ignore_secondaries=opts.ignore_secondaries,
663
                                  shutdown_timeout=opts.shutdown_timeout)
664

    
665

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

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

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

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

    
683

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

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

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

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

    
730

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

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

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

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

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

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

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

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

    
772

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

776
  The migrate is done without shutdown.
777

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

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

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

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

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

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

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

    
829

    
830
def MoveInstance(opts, args):
831
  """Move 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
  cl = GetClient()
841
  instance_name = args[0]
842
  force = opts.force
843

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

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

    
859

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

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

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

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

    
881
  del cl
882

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

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

    
895

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

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

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

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

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

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

    
945
  return constants.EXIT_SUCCESS
946

    
947

    
948
def _FormatLogicalID(dev_type, logical_id, roman):
949
  """Formats the logical_id of a disk.
950

951
  """
952
  if dev_type == constants.LD_DRBD8:
953
    node_a, node_b, port, minor_a, minor_b, key = logical_id
954
    data = [
955
      ("nodeA", "%s, minor=%s" % (node_a, compat.TryToRoman(minor_a,
956
                                                            convert=roman))),
957
      ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
958
                                                            convert=roman))),
959
      ("port", str(compat.TryToRoman(port, convert=roman))),
960
      ("auth key", str(key)),
961
      ]
962
  elif dev_type == constants.LD_LV:
963
    vg_name, lv_name = logical_id
964
    data = ["%s/%s" % (vg_name, lv_name)]
965
  else:
966
    data = [str(logical_id)]
967

    
968
  return data
969

    
970

    
971
def _FormatListInfo(data):
972
  return list(str(i) for i in data)
973

    
974

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1089

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

    
1107

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

    
1126

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

    
1151

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

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

    
1200

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

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

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

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

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

    
1235

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

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

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

1249
  """
1250
  result = []
1251

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

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

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

    
1284
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1285

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

    
1290
    result.append((action, identifier, params))
1291

    
1292
  return result
1293

    
1294

    
1295
def _ParseDiskSizes(mods):
1296
  """Parses disk sizes in parameters.
1297

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

    
1307
  return mods
1308

    
1309

    
1310
def SetInstanceParams(opts, args):
1311
  """Modifies an instance.
1312

1313
  All parameters take effect only at the next restart of the instance.
1314

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

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

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

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

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

    
1342
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1343
                      allowed_values=[constants.VALUE_DEFAULT])
1344

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

    
1354
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1355

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

    
1363
  if opts.offline_inst:
1364
    offline = True
1365
  elif opts.online_inst:
1366
    offline = False
1367
  else:
1368
    offline = None
1369

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

    
1390
  # even if here we process the result, we allow submit only
1391
  result = SubmitOrSend(op, opts)
1392

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

    
1403

    
1404
def ChangeGroup(opts, args):
1405
  """Moves an instance to another group.
1406

1407
  """
1408
  (instance_name, ) = args
1409

    
1410
  cl = GetClient()
1411

    
1412
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1413
                                     iallocator=opts.iallocator,
1414
                                     target_groups=opts.to,
1415
                                     early_release=opts.early_release)
1416
  result = SubmitOrSend(op, opts, cl=cl)
1417

    
1418
  # Keep track of submitted jobs
1419
  jex = JobExecutor(cl=cl, opts=opts)
1420

    
1421
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1422
    jex.AddJobId(None, status, job_id)
1423

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

    
1434
  return rcode
1435

    
1436

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

    
1443
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1444
                            help="Filter by nodes (primary only)",
1445
                            const=_EXPAND_NODES_PRI, action="store_const")
1446

    
1447
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1448
                            help="Filter by nodes (secondary only)",
1449
                            const=_EXPAND_NODES_SEC, action="store_const")
1450

    
1451
m_node_opt = cli_option("--node", dest="multi_mode",
1452
                        help="Filter by nodes (primary and secondary)",
1453
                        const=_EXPAND_NODES_BOTH, action="store_const")
1454

    
1455
m_clust_opt = cli_option("--all", dest="multi_mode",
1456
                         help="Select all instances in the cluster",
1457
                         const=_EXPAND_CLUSTER, action="store_const")
1458

    
1459
m_inst_opt = cli_option("--instance", dest="multi_mode",
1460
                        help="Filter by instance name [default]",
1461
                        const=_EXPAND_INSTANCES, action="store_const")
1462

    
1463
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1464
                             help="Filter by node tag",
1465
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1466
                             action="store_const")
1467

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

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

    
1478
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1479
                             help="Filter by instance tag",
1480
                             const=_EXPAND_INSTANCES_BY_TAGS,
1481
                             action="store_const")
1482

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

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

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

    
1650

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