Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ db5a8a2d

History | View | Annotate | Download (22.3 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
"""Node related commands"""
22

    
23
# pylint: disable-msg=W0401,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-node
28

    
29
import sys
30

    
31
from ganeti.cli import *
32
from ganeti import opcodes
33
from ganeti import utils
34
from ganeti import constants
35
from ganeti import compat
36
from ganeti import errors
37
from ganeti import bootstrap
38
from ganeti import netutils
39

    
40

    
41
#: default list of field for L{ListNodes}
42
_LIST_DEF_FIELDS = [
43
  "name", "dtotal", "dfree",
44
  "mtotal", "mnode", "mfree",
45
  "pinst_cnt", "sinst_cnt",
46
  ]
47

    
48

    
49
#: default list of field for L{ListStorage}
50
_LIST_STOR_DEF_FIELDS = [
51
  constants.SF_NODE,
52
  constants.SF_TYPE,
53
  constants.SF_NAME,
54
  constants.SF_SIZE,
55
  constants.SF_USED,
56
  constants.SF_FREE,
57
  constants.SF_ALLOCATABLE,
58
  ]
59

    
60

    
61
#: headers (and full field list for L{ListNodes}
62
_LIST_HEADERS = {
63
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
64
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
65
  "pip": "PrimaryIP", "sip": "SecondaryIP",
66
  "dtotal": "DTotal", "dfree": "DFree",
67
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
68
  "bootid": "BootID",
69
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
70
  "tags": "Tags",
71
  "serial_no": "SerialNo",
72
  "master_candidate": "MasterC",
73
  "master": "IsMaster",
74
  "offline": "Offline", "drained": "Drained",
75
  "role": "Role",
76
  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
77
  }
78

    
79

    
80
#: headers (and full field list for L{ListStorage}
81
_LIST_STOR_HEADERS = {
82
  constants.SF_NODE: "Node",
83
  constants.SF_TYPE: "Type",
84
  constants.SF_NAME: "Name",
85
  constants.SF_SIZE: "Size",
86
  constants.SF_USED: "Used",
87
  constants.SF_FREE: "Free",
88
  constants.SF_ALLOCATABLE: "Allocatable",
89
  }
90

    
91

    
92
#: User-facing storage unit types
93
_USER_STORAGE_TYPE = {
94
  constants.ST_FILE: "file",
95
  constants.ST_LVM_PV: "lvm-pv",
96
  constants.ST_LVM_VG: "lvm-vg",
97
  }
98

    
99
_STORAGE_TYPE_OPT = \
100
  cli_option("-t", "--storage-type",
101
             dest="user_storage_type",
102
             choices=_USER_STORAGE_TYPE.keys(),
103
             default=None,
104
             metavar="STORAGE_TYPE",
105
             help=("Storage type (%s)" %
106
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
107

    
108
_REPAIRABLE_STORAGE_TYPES = \
109
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
110
   if constants.SO_FIX_CONSISTENCY in so]
111

    
112
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
113

    
114

    
115
def ConvertStorageType(user_storage_type):
116
  """Converts a user storage type to its internal name.
117

    
118
  """
119
  try:
120
    return _USER_STORAGE_TYPE[user_storage_type]
121
  except KeyError:
122
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
123
                               errors.ECODE_INVAL)
124

    
125

    
126
@UsesRPC
127
def AddNode(opts, args):
128
  """Add a node to the cluster.
129

    
130
  @param opts: the command line options selected by the user
131
  @type args: list
132
  @param args: should contain only one element, the new node name
133
  @rtype: int
134
  @return: the desired exit code
135

    
136
  """
137
  cl = GetClient()
138
  dns_data = netutils.GetHostInfo(netutils.HostInfo.NormalizeName(args[0]))
139
  node = dns_data.name
140
  readd = opts.readd
141

    
142
  try:
143
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
144
                           use_locking=False)
145
    node_exists, sip = output[0]
146
  except (errors.OpPrereqError, errors.OpExecError):
147
    node_exists = ""
148
    sip = None
149

    
150
  if readd:
151
    if not node_exists:
152
      ToStderr("Node %s not in the cluster"
153
               " - please retry without '--readd'", node)
154
      return 1
155
  else:
156
    if node_exists:
157
      ToStderr("Node %s already in the cluster (as %s)"
158
               " - please retry with '--readd'", node, node_exists)
159
      return 1
160
    sip = opts.secondary_ip
161

    
162
  # read the cluster name from the master
163
  output = cl.QueryConfigValues(['cluster_name'])
164
  cluster_name = output[0]
165

    
166
  if not readd:
167
    ToStderr("-- WARNING -- \n"
168
             "Performing this operation is going to replace the ssh daemon"
169
             " keypair\n"
170
             "on the target machine (%s) with the ones of the"
171
             " current one\n"
172
             "and grant full intra-cluster ssh root access to/from it\n", node)
173

    
174
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
175

    
176
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
177
                         readd=opts.readd)
178
  SubmitOpCode(op, opts=opts)
179

    
180

    
181
def ListNodes(opts, args):
182
  """List nodes and their properties.
183

    
184
  @param opts: the command line options selected by the user
185
  @type args: list
186
  @param args: should be an empty list
187
  @rtype: int
188
  @return: the desired exit code
189

    
190
  """
191
  if opts.output is None:
192
    selected_fields = _LIST_DEF_FIELDS
193
  elif opts.output.startswith("+"):
194
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
195
  else:
196
    selected_fields = opts.output.split(",")
197

    
198
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
199

    
200
  if not opts.no_headers:
201
    headers = _LIST_HEADERS
202
  else:
203
    headers = None
204

    
205
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
206

    
207
  numfields = ["dtotal", "dfree",
208
               "mtotal", "mnode", "mfree",
209
               "pinst_cnt", "sinst_cnt",
210
               "ctotal", "serial_no"]
211

    
212
  list_type_fields = ("pinst_list", "sinst_list", "tags")
213
  # change raw values to nicer strings
214
  for row in output:
215
    for idx, field in enumerate(selected_fields):
216
      val = row[idx]
217
      if field in list_type_fields:
218
        val = ",".join(val)
219
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
220
        if val:
221
          val = 'Y'
222
        else:
223
          val = 'N'
224
      elif field == "ctime" or field == "mtime":
225
        val = utils.FormatTime(val)
226
      elif val is None:
227
        val = "?"
228
      elif opts.roman_integers and isinstance(val, int):
229
        val = compat.TryToRoman(val)
230
      row[idx] = str(val)
231

    
232
  data = GenerateTable(separator=opts.separator, headers=headers,
233
                       fields=selected_fields, unitfields=unitfields,
234
                       numfields=numfields, data=output, units=opts.units)
235
  for line in data:
236
    ToStdout(line)
237

    
238
  return 0
239

    
240

    
241
def EvacuateNode(opts, args):
242
  """Relocate all secondary instance from a node.
243

    
244
  @param opts: the command line options selected by the user
245
  @type args: list
246
  @param args: should be an empty list
247
  @rtype: int
248
  @return: the desired exit code
249

    
250
  """
251
  cl = GetClient()
252
  force = opts.force
253

    
254
  dst_node = opts.dst_node
255
  iallocator = opts.iallocator
256

    
257
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
258
                                        iallocator=iallocator,
259
                                        remote_node=dst_node)
260

    
261
  result = SubmitOpCode(op, cl=cl, opts=opts)
262
  if not result:
263
    # no instances to migrate
264
    ToStderr("No secondary instances on node(s) %s, exiting.",
265
             utils.CommaJoin(args))
266
    return constants.EXIT_SUCCESS
267

    
268
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
269
                               (",".join("'%s'" % name[0] for name in result),
270
                               utils.CommaJoin(args))):
271
    return constants.EXIT_CONFIRMATION
272

    
273
  jex = JobExecutor(cl=cl, opts=opts)
274
  for row in result:
275
    iname = row[0]
276
    node = row[1]
277
    ToStdout("Will relocate instance %s to node %s", iname, node)
278
    op = opcodes.OpReplaceDisks(instance_name=iname,
279
                                remote_node=node, disks=[],
280
                                mode=constants.REPLACE_DISK_CHG,
281
                                early_release=opts.early_release)
282
    jex.QueueJob(iname, op)
283
  results = jex.GetResults()
284
  bad_cnt = len([row for row in results if not row[0]])
285
  if bad_cnt == 0:
286
    ToStdout("All %d instance(s) failed over successfully.", len(results))
287
    rcode = constants.EXIT_SUCCESS
288
  else:
289
    ToStdout("There were errors during the failover:\n"
290
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
291
    rcode = constants.EXIT_FAILURE
292
  return rcode
293

    
294

    
295
def FailoverNode(opts, args):
296
  """Failover all primary instance on a node.
297

    
298
  @param opts: the command line options selected by the user
299
  @type args: list
300
  @param args: should be an empty list
301
  @rtype: int
302
  @return: the desired exit code
303

    
304
  """
305
  cl = GetClient()
306
  force = opts.force
307
  selected_fields = ["name", "pinst_list"]
308

    
309
  # these fields are static data anyway, so it doesn't matter, but
310
  # locking=True should be safer
311
  result = cl.QueryNodes(names=args, fields=selected_fields,
312
                         use_locking=False)
313
  node, pinst = result[0]
314

    
315
  if not pinst:
316
    ToStderr("No primary instances on node %s, exiting.", node)
317
    return 0
318

    
319
  pinst = utils.NiceSort(pinst)
320

    
321
  retcode = 0
322

    
323
  if not force and not AskUser("Fail over instance(s) %s?" %
324
                               (",".join("'%s'" % name for name in pinst))):
325
    return 2
326

    
327
  jex = JobExecutor(cl=cl, opts=opts)
328
  for iname in pinst:
329
    op = opcodes.OpFailoverInstance(instance_name=iname,
330
                                    ignore_consistency=opts.ignore_consistency)
331
    jex.QueueJob(iname, op)
332
  results = jex.GetResults()
333
  bad_cnt = len([row for row in results if not row[0]])
334
  if bad_cnt == 0:
335
    ToStdout("All %d instance(s) failed over successfully.", len(results))
336
  else:
337
    ToStdout("There were errors during the failover:\n"
338
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
339
  return retcode
340

    
341

    
342
def MigrateNode(opts, args):
343
  """Migrate all primary instance on a node.
344

    
345
  """
346
  cl = GetClient()
347
  force = opts.force
348
  selected_fields = ["name", "pinst_list"]
349

    
350
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
351
  node, pinst = result[0]
352

    
353
  if not pinst:
354
    ToStdout("No primary instances on node %s, exiting." % node)
355
    return 0
356

    
357
  pinst = utils.NiceSort(pinst)
358

    
359
  if not force and not AskUser("Migrate instance(s) %s?" %
360
                               (",".join("'%s'" % name for name in pinst))):
361
    return 2
362

    
363
  # this should be removed once --non-live is deprecated
364
  if not opts.live and opts.migration_mode is not None:
365
    raise errors.OpPrereqError("Only one of the --non-live and "
366
                               "--migration-mode options can be passed",
367
                               errors.ECODE_INVAL)
368
  if not opts.live: # --non-live passed
369
    mode = constants.HT_MIGRATION_NONLIVE
370
  else:
371
    mode = opts.migration_mode
372
  op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
373
  SubmitOpCode(op, cl=cl, opts=opts)
374

    
375

    
376
def ShowNodeConfig(opts, args):
377
  """Show node information.
378

    
379
  @param opts: the command line options selected by the user
380
  @type args: list
381
  @param args: should either be an empty list, in which case
382
      we show information about all nodes, or should contain
383
      a list of nodes to be queried for information
384
  @rtype: int
385
  @return: the desired exit code
386

    
387
  """
388
  cl = GetClient()
389
  result = cl.QueryNodes(fields=["name", "pip", "sip",
390
                                 "pinst_list", "sinst_list",
391
                                 "master_candidate", "drained", "offline"],
392
                         names=args, use_locking=False)
393

    
394
  for (name, primary_ip, secondary_ip, pinst, sinst,
395
       is_mc, drained, offline) in result:
396
    ToStdout("Node name: %s", name)
397
    ToStdout("  primary ip: %s", primary_ip)
398
    ToStdout("  secondary ip: %s", secondary_ip)
399
    ToStdout("  master candidate: %s", is_mc)
400
    ToStdout("  drained: %s", drained)
401
    ToStdout("  offline: %s", offline)
402
    if pinst:
403
      ToStdout("  primary for instances:")
404
      for iname in utils.NiceSort(pinst):
405
        ToStdout("    - %s", iname)
406
    else:
407
      ToStdout("  primary for no instances")
408
    if sinst:
409
      ToStdout("  secondary for instances:")
410
      for iname in utils.NiceSort(sinst):
411
        ToStdout("    - %s", iname)
412
    else:
413
      ToStdout("  secondary for no instances")
414

    
415
  return 0
416

    
417

    
418
def RemoveNode(opts, args):
419
  """Remove a node from the cluster.
420

    
421
  @param opts: the command line options selected by the user
422
  @type args: list
423
  @param args: should contain only one element, the name of
424
      the node to be removed
425
  @rtype: int
426
  @return: the desired exit code
427

    
428
  """
429
  op = opcodes.OpRemoveNode(node_name=args[0])
430
  SubmitOpCode(op, opts=opts)
431
  return 0
432

    
433

    
434
def PowercycleNode(opts, args):
435
  """Remove a node from the cluster.
436

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

    
444
  """
445
  node = args[0]
446
  if (not opts.confirm and
447
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
448
    return 2
449

    
450
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
451
  result = SubmitOpCode(op, opts=opts)
452
  if result:
453
    ToStderr(result)
454
  return 0
455

    
456

    
457
def ListVolumes(opts, args):
458
  """List logical volumes on node(s).
459

    
460
  @param opts: the command line options selected by the user
461
  @type args: list
462
  @param args: should either be an empty list, in which case
463
      we list data for all nodes, or contain a list of nodes
464
      to display data only for those
465
  @rtype: int
466
  @return: the desired exit code
467

    
468
  """
469
  if opts.output is None:
470
    selected_fields = ["node", "phys", "vg",
471
                       "name", "size", "instance"]
472
  else:
473
    selected_fields = opts.output.split(",")
474

    
475
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
476
  output = SubmitOpCode(op, opts=opts)
477

    
478
  if not opts.no_headers:
479
    headers = {"node": "Node", "phys": "PhysDev",
480
               "vg": "VG", "name": "Name",
481
               "size": "Size", "instance": "Instance"}
482
  else:
483
    headers = None
484

    
485
  unitfields = ["size"]
486

    
487
  numfields = ["size"]
488

    
489
  data = GenerateTable(separator=opts.separator, headers=headers,
490
                       fields=selected_fields, unitfields=unitfields,
491
                       numfields=numfields, data=output, units=opts.units)
492

    
493
  for line in data:
494
    ToStdout(line)
495

    
496
  return 0
497

    
498

    
499
def ListStorage(opts, args):
500
  """List physical volumes on node(s).
501

    
502
  @param opts: the command line options selected by the user
503
  @type args: list
504
  @param args: should either be an empty list, in which case
505
      we list data for all nodes, or contain a list of nodes
506
      to display data only for those
507
  @rtype: int
508
  @return: the desired exit code
509

    
510
  """
511
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
512
  if opts.user_storage_type is None:
513
    opts.user_storage_type = constants.ST_LVM_PV
514

    
515
  storage_type = ConvertStorageType(opts.user_storage_type)
516

    
517
  if opts.output is None:
518
    selected_fields = _LIST_STOR_DEF_FIELDS
519
  elif opts.output.startswith("+"):
520
    selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
521
  else:
522
    selected_fields = opts.output.split(",")
523

    
524
  op = opcodes.OpQueryNodeStorage(nodes=args,
525
                                  storage_type=storage_type,
526
                                  output_fields=selected_fields)
527
  output = SubmitOpCode(op, opts=opts)
528

    
529
  if not opts.no_headers:
530
    headers = {
531
      constants.SF_NODE: "Node",
532
      constants.SF_TYPE: "Type",
533
      constants.SF_NAME: "Name",
534
      constants.SF_SIZE: "Size",
535
      constants.SF_USED: "Used",
536
      constants.SF_FREE: "Free",
537
      constants.SF_ALLOCATABLE: "Allocatable",
538
      }
539
  else:
540
    headers = None
541

    
542
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
543
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
544

    
545
  # change raw values to nicer strings
546
  for row in output:
547
    for idx, field in enumerate(selected_fields):
548
      val = row[idx]
549
      if field == constants.SF_ALLOCATABLE:
550
        if val:
551
          val = "Y"
552
        else:
553
          val = "N"
554
      row[idx] = str(val)
555

    
556
  data = GenerateTable(separator=opts.separator, headers=headers,
557
                       fields=selected_fields, unitfields=unitfields,
558
                       numfields=numfields, data=output, units=opts.units)
559

    
560
  for line in data:
561
    ToStdout(line)
562

    
563
  return 0
564

    
565

    
566
def ModifyStorage(opts, args):
567
  """Modify storage volume on a node.
568

    
569
  @param opts: the command line options selected by the user
570
  @type args: list
571
  @param args: should contain 3 items: node name, storage type and volume name
572
  @rtype: int
573
  @return: the desired exit code
574

    
575
  """
576
  (node_name, user_storage_type, volume_name) = args
577

    
578
  storage_type = ConvertStorageType(user_storage_type)
579

    
580
  changes = {}
581

    
582
  if opts.allocatable is not None:
583
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
584

    
585
  if changes:
586
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
587
                                     storage_type=storage_type,
588
                                     name=volume_name,
589
                                     changes=changes)
590
    SubmitOpCode(op, opts=opts)
591
  else:
592
    ToStderr("No changes to perform, exiting.")
593

    
594

    
595
def RepairStorage(opts, args):
596
  """Repairs a storage volume on a node.
597

    
598
  @param opts: the command line options selected by the user
599
  @type args: list
600
  @param args: should contain 3 items: node name, storage type and volume name
601
  @rtype: int
602
  @return: the desired exit code
603

    
604
  """
605
  (node_name, user_storage_type, volume_name) = args
606

    
607
  storage_type = ConvertStorageType(user_storage_type)
608

    
609
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
610
                                   storage_type=storage_type,
611
                                   name=volume_name,
612
                                   ignore_consistency=opts.ignore_consistency)
613
  SubmitOpCode(op, opts=opts)
614

    
615

    
616
def SetNodeParams(opts, args):
617
  """Modifies a node.
618

    
619
  @param opts: the command line options selected by the user
620
  @type args: list
621
  @param args: should contain only one element, the node name
622
  @rtype: int
623
  @return: the desired exit code
624

    
625
  """
626
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
627
    ToStderr("Please give at least one of the parameters.")
628
    return 1
629

    
630
  op = opcodes.OpSetNodeParams(node_name=args[0],
631
                               master_candidate=opts.master_candidate,
632
                               offline=opts.offline,
633
                               drained=opts.drained,
634
                               force=opts.force,
635
                               auto_promote=opts.auto_promote)
636

    
637
  # even if here we process the result, we allow submit only
638
  result = SubmitOrSend(op, opts)
639

    
640
  if result:
641
    ToStdout("Modified node %s", args[0])
642
    for param, data in result:
643
      ToStdout(" - %-5s -> %s", param, data)
644
  return 0
645

    
646

    
647
commands = {
648
  'add': (
649
    AddNode, [ArgHost(min=1, max=1)],
650
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
651
    "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
652
    "Add a node to the cluster"),
653
  'evacuate': (
654
    EvacuateNode, [ArgNode(min=1)],
655
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
656
    "[-f] {-I <iallocator> | -n <dst>} <node>",
657
    "Relocate the secondary instances from a node"
658
    " to other nodes (only for instances with drbd disk template)"),
659
  'failover': (
660
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
661
    "[-f] <node>",
662
    "Stops the primary instances on a node and start them on their"
663
    " secondary node (only for instances with drbd disk template)"),
664
  'migrate': (
665
    MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
666
    "[-f] <node>",
667
    "Migrate all the primary instance on a node away from it"
668
    " (only for instances of type drbd)"),
669
  'info': (
670
    ShowNodeConfig, ARGS_MANY_NODES, [],
671
    "[<node_name>...]", "Show information about the node(s)"),
672
  'list': (
673
    ListNodes, ARGS_MANY_NODES,
674
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
675
    "[nodes...]",
676
    "Lists the nodes in the cluster. The available fields are (see the man"
677
    " page for details): %s. The default field list is (in order): %s." %
678
    (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
679
  'modify': (
680
    SetNodeParams, ARGS_ONE_NODE,
681
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
682
     AUTO_PROMOTE_OPT, DRY_RUN_OPT],
683
    "<node_name>", "Alters the parameters of a node"),
684
  'powercycle': (
685
    PowercycleNode, ARGS_ONE_NODE,
686
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT],
687
    "<node_name>", "Tries to forcefully powercycle a node"),
688
  'remove': (
689
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT],
690
    "<node_name>", "Removes a node from the cluster"),
691
  'volumes': (
692
    ListVolumes, [ArgNode()],
693
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
694
    "[<node_name>...]", "List logical volumes on node(s)"),
695
  'list-storage': (
696
    ListStorage, ARGS_MANY_NODES,
697
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
698
    "[<node_name>...]", "List physical volumes on node(s). The available"
699
    " fields are (see the man page for details): %s." %
700
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
701
  'modify-storage': (
702
    ModifyStorage,
703
    [ArgNode(min=1, max=1),
704
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
705
     ArgFile(min=1, max=1)],
706
    [ALLOCATABLE_OPT, DRY_RUN_OPT],
707
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
708
  'repair-storage': (
709
    RepairStorage,
710
    [ArgNode(min=1, max=1),
711
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
712
     ArgFile(min=1, max=1)],
713
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT],
714
    "<node_name> <storage_type> <name>",
715
    "Repairs a storage volume on a node"),
716
  'list-tags': (
717
    ListTags, ARGS_ONE_NODE, [],
718
    "<node_name>", "List the tags of the given node"),
719
  'add-tags': (
720
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
721
    "<node_name> tag...", "Add tags to the given node"),
722
  'remove-tags': (
723
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
724
    "<node_name> tag...", "Remove tags from the given node"),
725
  }
726

    
727

    
728
if __name__ == '__main__':
729
  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))