Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_instance.py @ 3779121c

History | View | Annotate | Download (55.9 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
from cStringIO import StringIO
33

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

    
45

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

    
56
_EXPAND_NODES_TAGS_MODES = frozenset([
57
  _EXPAND_NODES_BOTH_BY_TAGS,
58
  _EXPAND_NODES_PRI_BY_TAGS,
59
  _EXPAND_NODES_SEC_BY_TAGS,
60
  ])
61

    
62

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

    
68

    
69
_MISSING = object()
70
_ENV_OVERRIDE = frozenset(["list"])
71

    
72
_INST_DATA_VAL = ht.TListOf(ht.TDict)
73

    
74

    
75
def _ExpandMultiNames(mode, names, client=None):
76
  """Expand the given names using the passed mode.
77

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

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

97
  """
98
  # pylint: disable=W0142
99

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

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

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

    
150
  return inames
151

    
152

    
153
def _EnsureInstancesExist(client, names):
154
  """Check for and ensure the given instance names exist.
155

156
  This function will raise an OpPrereqError in case they don't
157
  exist. Otherwise it will exit cleanly.
158

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

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

    
174

    
175
def GenericManyOps(operation, fn):
176
  """Generic multi-instance operations.
177

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

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

    
208

    
209
def ListInstances(opts, args):
210
  """List instances and their properties.
211

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

218
  """
219
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
220

    
221
  fmtoverride = dict.fromkeys(["tags", "disk.sizes", "nic.macs", "nic.ips",
222
                               "nic.modes", "nic.links", "nic.bridges",
223
                               "snodes", "snodes.group", "snodes.group.uuid"],
224
                              (lambda value: ",".join(str(item)
225
                                                      for item in value),
226
                               False))
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)
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
  jex.WaitOrShow(not opts.submit_only)
401
  return 0
402

    
403

    
404
def RemoveInstance(opts, args):
405
  """Remove an instance.
406

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

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

    
419
  if not force:
420
    _EnsureInstancesExist(cl, [instance_name])
421

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

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

    
434

    
435
def RenameInstance(opts, args):
436
  """Rename an instance.
437

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

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

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

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

    
460
  return 0
461

    
462

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

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

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

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

    
487

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

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

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

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

    
507

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

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

517
  """
518
  instance_name = args[0]
519

    
520
  disks = []
521

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

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

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

    
538
      disks.append((didx, ddict))
539

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

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

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

    
559
  return 0
560

    
561

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

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

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

    
592

    
593
def _StartupInstance(name, opts):
594
  """Startup instances.
595

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

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

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

    
616

    
617
def _RebootInstance(name, opts):
618
  """Reboot instance(s).
619

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

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

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

    
633

    
634
def _ShutdownInstance(name, opts):
635
  """Shutdown an instance.
636

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

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

644
  """
645
  return opcodes.OpInstanceShutdown(instance_name=name,
646
                                    timeout=opts.timeout,
647
                                    ignore_offline_nodes=opts.ignore_offline,
648
                                    no_remember=opts.no_remember)
649

    
650

    
651
def ReplaceDisks(opts, args):
652
  """Replace the disks of an instance
653

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

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

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

    
697

    
698
def FailoverInstance(opts, args):
699
  """Failover an instance.
700

701
  The failover is done by shutting it down on its present node and
702
  starting it on the secondary.
703

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

710
  """
711
  cl = GetClient()
712
  instance_name = args[0]
713
  force = opts.force
714
  iallocator = opts.iallocator
715
  target_node = opts.dst_node
716

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

    
721
  if not force:
722
    _EnsureInstancesExist(cl, [instance_name])
723

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

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

    
739

    
740
def MigrateInstance(opts, args):
741
  """Migrate an instance.
742

743
  The migrate is done without shutdown.
744

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

751
  """
752
  cl = GetClient()
753
  instance_name = args[0]
754
  force = opts.force
755
  iallocator = opts.iallocator
756
  target_node = opts.dst_node
757

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

    
762
  if not force:
763
    _EnsureInstancesExist(cl, [instance_name])
764

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

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

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

    
796

    
797
def MoveInstance(opts, args):
798
  """Move an instance.
799

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

806
  """
807
  cl = GetClient()
808
  instance_name = args[0]
809
  force = opts.force
810

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

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

    
826

    
827
def ConnectToInstanceConsole(opts, args):
828
  """Connect to the console of an instance.
829

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

836
  """
837
  instance_name = args[0]
838

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

    
848
  del cl
849

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

    
859
  return _DoConsole(objects.InstanceConsole.FromDict(console_data),
860
                    opts.show_command, cluster_name)
861

    
862

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

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

874
  """
875
  assert console.Validate()
876

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

    
894
    srun = ssh.SshRunner(cluster_name=cluster_name)
895
    ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
896
                            batch=True, quiet=False, tty=True)
897

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

    
912
  return constants.EXIT_SUCCESS
913

    
914

    
915
def _FormatLogicalID(dev_type, logical_id, roman):
916
  """Formats the logical_id of a disk.
917

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

    
935
  return data
936

    
937

    
938
def _FormatBlockDevInfo(idx, top_level, dev, roman):
939
  """Show block device information.
940

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

944
  @type idx: int
945
  @param idx: the index of the current disk
946
  @type top_level: boolean
947
  @param top_level: if this a top-level disk?
948
  @type dev: dict
949
  @param dev: dictionary with disk information
950
  @type roman: boolean
951
  @param roman: whether to try to use roman integers
952
  @return: a list of either strings, tuples or lists
953
      (which should be formatted at a higher indent level)
954

955
  """
956
  def helper(dtype, status):
957
    """Format one line for physical device status.
958

959
    @type dtype: str
960
    @param dtype: a constant from the L{constants.LDS_BLOCK} set
961
    @type status: tuple
962
    @param status: a tuple as returned from L{backend.FindBlockDevice}
963
    @return: the string representing the status
964

965
    """
966
    if not status:
967
      return "not active"
968
    txt = ""
969
    (path, major, minor, syncp, estt, degr, ldisk_status) = status
970
    if major is None:
971
      major_string = "N/A"
972
    else:
973
      major_string = str(compat.TryToRoman(major, convert=roman))
974

    
975
    if minor is None:
976
      minor_string = "N/A"
977
    else:
978
      minor_string = str(compat.TryToRoman(minor, convert=roman))
979

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

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

    
1038
  if dev["pstatus"]:
1039
    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
1040

    
1041
  if dev["sstatus"]:
1042
    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
1043

    
1044
  if dev["children"]:
1045
    data.append("child devices:")
1046
    for c_idx, child in enumerate(dev["children"]):
1047
      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
1048
  d1.append(data)
1049
  return d1
1050

    
1051

    
1052
def _FormatList(buf, data, indent_level):
1053
  """Formats a list of data at a given indent level.
1054

1055
  If the element of the list is:
1056
    - a string, it is simply formatted as is
1057
    - a tuple, it will be split into key, value and the all the
1058
      values in a list will be aligned all at the same start column
1059
    - a list, will be recursively formatted
1060

1061
  @type buf: StringIO
1062
  @param buf: the buffer into which we write the output
1063
  @param data: the list to format
1064
  @type indent_level: int
1065
  @param indent_level: the indent level to format at
1066

1067
  """
1068
  max_tlen = max([len(elem[0]) for elem in data
1069
                 if isinstance(elem, tuple)] or [0])
1070
  for elem in data:
1071
    if isinstance(elem, basestring):
1072
      buf.write("%*s%s\n" % (2 * indent_level, "", elem))
1073
    elif isinstance(elem, tuple):
1074
      key, value = elem
1075
      spacer = "%*s" % (max_tlen - len(key), "")
1076
      buf.write("%*s%s:%s %s\n" % (2 * indent_level, "", key, spacer, value))
1077
    elif isinstance(elem, list):
1078
      _FormatList(buf, elem, indent_level + 1)
1079

    
1080

    
1081
def ShowInstanceConfig(opts, args):
1082
  """Compute instance run-time status.
1083

1084
  @param opts: the command line options selected by the user
1085
  @type args: list
1086
  @param args: either an empty list, and then we query all
1087
      instances, or should contain a list of instance names
1088
  @rtype: int
1089
  @return: the desired exit code
1090

1091
  """
1092
  if not args and not opts.show_all:
1093
    ToStderr("No instance selected."
1094
             " Please pass in --all if you want to query all instances.\n"
1095
             "Note that this can take a long time on a big cluster.")
1096
    return 1
1097
  elif args and opts.show_all:
1098
    ToStderr("Cannot use --all if you specify instance names.")
1099
    return 1
1100

    
1101
  retcode = 0
1102
  op = opcodes.OpInstanceQueryData(instances=args, static=opts.static,
1103
                                   use_locking=not opts.static)
1104
  result = SubmitOpCode(op, opts=opts)
1105
  if not result:
1106
    ToStdout("No instances.")
1107
    return 1
1108

    
1109
  buf = StringIO()
1110
  retcode = 0
1111
  for instance_name in result:
1112
    instance = result[instance_name]
1113
    buf.write("Instance name: %s\n" % instance["name"])
1114
    buf.write("UUID: %s\n" % instance["uuid"])
1115
    buf.write("Serial number: %s\n" %
1116
              compat.TryToRoman(instance["serial_no"],
1117
                                convert=opts.roman_integers))
1118
    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
1119
    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
1120
    buf.write("State: configured to be %s" % instance["config_state"])
1121
    if instance["run_state"]:
1122
      buf.write(", actual state is %s" % instance["run_state"])
1123
    buf.write("\n")
1124
    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
1125
    ##          instance["auto_balance"])
1126
    buf.write("  Nodes:\n")
1127
    buf.write("    - primary: %s\n" % instance["pnode"])
1128
    buf.write("      group: %s (UUID %s)\n" %
1129
              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
1130
    buf.write("    - secondaries: %s\n" %
1131
              utils.CommaJoin("%s (group %s, group UUID %s)" %
1132
                                (name, group_name, group_uuid)
1133
                              for (name, group_name, group_uuid) in
1134
                                zip(instance["snodes"],
1135
                                    instance["snodes_group_names"],
1136
                                    instance["snodes_group_uuids"])))
1137
    buf.write("  Operating system: %s\n" % instance["os"])
1138
    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
1139
                        level=2)
1140
    if "network_port" in instance:
1141
      buf.write("  Allocated network port: %s\n" %
1142
                compat.TryToRoman(instance["network_port"],
1143
                                  convert=opts.roman_integers))
1144
    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
1145

    
1146
    # custom VNC console information
1147
    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
1148
                                                 None)
1149
    if vnc_bind_address:
1150
      port = instance["network_port"]
1151
      display = int(port) - constants.VNC_BASE_PORT
1152
      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
1153
        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
1154
                                                   port,
1155
                                                   display)
1156
      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
1157
        vnc_console_port = ("%s:%s (node %s) (display %s)" %
1158
                             (vnc_bind_address, port,
1159
                              instance["pnode"], display))
1160
      else:
1161
        # vnc bind address is a file
1162
        vnc_console_port = "%s:%s" % (instance["pnode"],
1163
                                      vnc_bind_address)
1164
      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
1165

    
1166
    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
1167
                        level=2)
1168
    buf.write("  Hardware:\n")
1169
    # deprecated "memory" value, kept for one version for compatibility
1170
    # TODO(ganeti 2.7) remove.
1171
    be_actual = copy.deepcopy(instance["be_actual"])
1172
    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
1173
    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
1174
    # TODO(ganeti 2.7) rework the NICs as well
1175
    buf.write("    - NICs:\n")
1176
    for idx, (ip, mac, mode, link) in enumerate(instance["nics"]):
1177
      buf.write("      - nic/%d: MAC: %s, IP: %s, mode: %s, link: %s\n" %
1178
                (idx, mac, ip, mode, link))
1179
    buf.write("  Disk template: %s\n" % instance["disk_template"])
1180
    buf.write("  Disks:\n")
1181

    
1182
    for idx, device in enumerate(instance["disks"]):
1183
      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
1184
                  opts.roman_integers), 2)
1185

    
1186
  ToStdout(buf.getvalue().rstrip("\n"))
1187
  return retcode
1188

    
1189

    
1190
def _ConvertNicDiskModifications(mods):
1191
  """Converts NIC/disk modifications from CLI to opcode.
1192

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

1198
  @type mods: list of tuples
1199
  @param mods: Modifications as given by command line parser
1200
  @rtype: list of tuples
1201
  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
1202

1203
  """
1204
  result = []
1205

    
1206
  for (idx, params) in mods:
1207
    if idx == constants.DDM_ADD:
1208
      # Add item as last item (legacy interface)
1209
      action = constants.DDM_ADD
1210
      idxno = -1
1211
    elif idx == constants.DDM_REMOVE:
1212
      # Remove last item (legacy interface)
1213
      action = constants.DDM_REMOVE
1214
      idxno = -1
1215
    else:
1216
      # Modifications and adding/removing at arbitrary indices
1217
      try:
1218
        idxno = int(idx)
1219
      except (TypeError, ValueError):
1220
        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
1221
                                   errors.ECODE_INVAL)
1222

    
1223
      add = params.pop(constants.DDM_ADD, _MISSING)
1224
      remove = params.pop(constants.DDM_REMOVE, _MISSING)
1225
      modify = params.pop(constants.DDM_MODIFY, _MISSING)
1226

    
1227
      if modify is _MISSING:
1228
        if not (add is _MISSING or remove is _MISSING):
1229
          raise errors.OpPrereqError("Cannot add and remove at the same time",
1230
                                     errors.ECODE_INVAL)
1231
        elif add is not _MISSING:
1232
          action = constants.DDM_ADD
1233
        elif remove is not _MISSING:
1234
          action = constants.DDM_REMOVE
1235
        else:
1236
          action = constants.DDM_MODIFY
1237

    
1238
      else:
1239
        if add is _MISSING and remove is _MISSING:
1240
          action = constants.DDM_MODIFY
1241
        else:
1242
          raise errors.OpPrereqError("Cannot modify and add/remove at the"
1243
                                     " same time", errors.ECODE_INVAL)
1244

    
1245
      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
1246

    
1247
    if action == constants.DDM_REMOVE and params:
1248
      raise errors.OpPrereqError("Not accepting parameters on removal",
1249
                                 errors.ECODE_INVAL)
1250

    
1251
    result.append((action, idxno, params))
1252

    
1253
  return result
1254

    
1255

    
1256
def _ParseDiskSizes(mods):
1257
  """Parses disk sizes in parameters.
1258

1259
  """
1260
  for (action, _, params) in mods:
1261
    if params and constants.IDISK_SIZE in params:
1262
      params[constants.IDISK_SIZE] = \
1263
        utils.ParseUnit(params[constants.IDISK_SIZE])
1264
    elif action == constants.DDM_ADD:
1265
      raise errors.OpPrereqError("Missing required parameter 'size'",
1266
                                 errors.ECODE_INVAL)
1267

    
1268
  return mods
1269

    
1270

    
1271
def SetInstanceParams(opts, args):
1272
  """Modifies an instance.
1273

1274
  All parameters take effect only at the next restart of the instance.
1275

1276
  @param opts: the command line options selected by the user
1277
  @type args: list
1278
  @param args: should contain only one element, the instance name
1279
  @rtype: int
1280
  @return: the desired exit code
1281

1282
  """
1283
  if not (opts.nics or opts.disks or opts.disk_template or
1284
          opts.hvparams or opts.beparams or opts.os or opts.osparams or
1285
          opts.offline_inst or opts.online_inst or opts.runtime_mem):
1286
    ToStderr("Please give at least one of the parameters.")
1287
    return 1
1288

    
1289
  for param in opts.beparams:
1290
    if isinstance(opts.beparams[param], basestring):
1291
      if opts.beparams[param].lower() == "default":
1292
        opts.beparams[param] = constants.VALUE_DEFAULT
1293

    
1294
  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
1295
                      allowed_values=[constants.VALUE_DEFAULT])
1296

    
1297
  for param in opts.hvparams:
1298
    if isinstance(opts.hvparams[param], basestring):
1299
      if opts.hvparams[param].lower() == "default":
1300
        opts.hvparams[param] = constants.VALUE_DEFAULT
1301

    
1302
  utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
1303
                      allowed_values=[constants.VALUE_DEFAULT])
1304

    
1305
  nics = _ConvertNicDiskModifications(opts.nics)
1306
  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
1307

    
1308
  if (opts.disk_template and
1309
      opts.disk_template in constants.DTS_INT_MIRROR and
1310
      not opts.node):
1311
    ToStderr("Changing the disk template to a mirrored one requires"
1312
             " specifying a secondary node")
1313
    return 1
1314

    
1315
  if opts.offline_inst:
1316
    offline = True
1317
  elif opts.online_inst:
1318
    offline = False
1319
  else:
1320
    offline = None
1321

    
1322
  op = opcodes.OpInstanceSetParams(instance_name=args[0],
1323
                                   nics=nics,
1324
                                   disks=disks,
1325
                                   disk_template=opts.disk_template,
1326
                                   remote_node=opts.node,
1327
                                   hvparams=opts.hvparams,
1328
                                   beparams=opts.beparams,
1329
                                   runtime_mem=opts.runtime_mem,
1330
                                   os_name=opts.os,
1331
                                   osparams=opts.osparams,
1332
                                   force_variant=opts.force_variant,
1333
                                   force=opts.force,
1334
                                   wait_for_sync=opts.wait_for_sync,
1335
                                   offline=offline,
1336
                                   ignore_ipolicy=opts.ignore_ipolicy)
1337

    
1338
  # even if here we process the result, we allow submit only
1339
  result = SubmitOrSend(op, opts)
1340

    
1341
  if result:
1342
    ToStdout("Modified instance %s", args[0])
1343
    for param, data in result:
1344
      ToStdout(" - %-5s -> %s", param, data)
1345
    ToStdout("Please don't forget that most parameters take effect"
1346
             " only at the next start of the instance.")
1347
  return 0
1348

    
1349

    
1350
def ChangeGroup(opts, args):
1351
  """Moves an instance to another group.
1352

1353
  """
1354
  (instance_name, ) = args
1355

    
1356
  cl = GetClient()
1357

    
1358
  op = opcodes.OpInstanceChangeGroup(instance_name=instance_name,
1359
                                     iallocator=opts.iallocator,
1360
                                     target_groups=opts.to,
1361
                                     early_release=opts.early_release)
1362
  result = SubmitOrSend(op, opts, cl=cl)
1363

    
1364
  # Keep track of submitted jobs
1365
  jex = JobExecutor(cl=cl, opts=opts)
1366

    
1367
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
1368
    jex.AddJobId(None, status, job_id)
1369

    
1370
  results = jex.GetResults()
1371
  bad_cnt = len([row for row in results if not row[0]])
1372
  if bad_cnt == 0:
1373
    ToStdout("Instance '%s' changed group successfully.", instance_name)
1374
    rcode = constants.EXIT_SUCCESS
1375
  else:
1376
    ToStdout("There were %s errors while changing group of instance '%s'.",
1377
             bad_cnt, instance_name)
1378
    rcode = constants.EXIT_FAILURE
1379

    
1380
  return rcode
1381

    
1382

    
1383
# multi-instance selection options
1384
m_force_multi = cli_option("--force-multiple", dest="force_multi",
1385
                           help="Do not ask for confirmation when more than"
1386
                           " one instance is affected",
1387
                           action="store_true", default=False)
1388

    
1389
m_pri_node_opt = cli_option("--primary", dest="multi_mode",
1390
                            help="Filter by nodes (primary only)",
1391
                            const=_EXPAND_NODES_PRI, action="store_const")
1392

    
1393
m_sec_node_opt = cli_option("--secondary", dest="multi_mode",
1394
                            help="Filter by nodes (secondary only)",
1395
                            const=_EXPAND_NODES_SEC, action="store_const")
1396

    
1397
m_node_opt = cli_option("--node", dest="multi_mode",
1398
                        help="Filter by nodes (primary and secondary)",
1399
                        const=_EXPAND_NODES_BOTH, action="store_const")
1400

    
1401
m_clust_opt = cli_option("--all", dest="multi_mode",
1402
                         help="Select all instances in the cluster",
1403
                         const=_EXPAND_CLUSTER, action="store_const")
1404

    
1405
m_inst_opt = cli_option("--instance", dest="multi_mode",
1406
                        help="Filter by instance name [default]",
1407
                        const=_EXPAND_INSTANCES, action="store_const")
1408

    
1409
m_node_tags_opt = cli_option("--node-tags", dest="multi_mode",
1410
                             help="Filter by node tag",
1411
                             const=_EXPAND_NODES_BOTH_BY_TAGS,
1412
                             action="store_const")
1413

    
1414
m_pri_node_tags_opt = cli_option("--pri-node-tags", dest="multi_mode",
1415
                                 help="Filter by primary node tag",
1416
                                 const=_EXPAND_NODES_PRI_BY_TAGS,
1417
                                 action="store_const")
1418

    
1419
m_sec_node_tags_opt = cli_option("--sec-node-tags", dest="multi_mode",
1420
                                 help="Filter by secondary node tag",
1421
                                 const=_EXPAND_NODES_SEC_BY_TAGS,
1422
                                 action="store_const")
1423

    
1424
m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
1425
                             help="Filter by instance tag",
1426
                             const=_EXPAND_INSTANCES_BY_TAGS,
1427
                             action="store_const")
1428

    
1429
# this is defined separately due to readability only
1430
add_opts = [
1431
  NOSTART_OPT,
1432
  OS_OPT,
1433
  FORCE_VARIANT_OPT,
1434
  NO_INSTALL_OPT,
1435
  IGNORE_IPOLICY_OPT,
1436
  ]
1437

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

    
1583
#: dictionary with aliases for commands
1584
aliases = {
1585
  "start": "startup",
1586
  "stop": "shutdown",
1587
  "show": "info",
1588
  }
1589

    
1590

    
1591
def Main():
1592
  return GenericMain(commands, aliases=aliases,
1593
                     override={"tag_type": constants.TAG_INSTANCE},
1594
                     env_override=_ENV_OVERRIDE)