Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 2d54e29c

History | View | Annotate | Download (21.7 kB)

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

    
4
# Copyright (C) 2006, 2007, 2008 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 cli
33
from ganeti import opcodes
34
from ganeti import utils
35
from ganeti import constants
36
from ganeti import errors
37
from ganeti import bootstrap
38

    
39

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

    
47

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

    
59

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

    
78

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

    
90

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

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

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

    
111
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
112

    
113

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

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

    
124

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

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

    
135
  """
136
  cl = GetClient()
137
  dns_data = utils.GetHostInfo(args[0])
138
  node = dns_data.name
139
  readd = opts.readd
140

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

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

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

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

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

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

    
179

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

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

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

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

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

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

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

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

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

    
235
  return 0
236

    
237

    
238
def EvacuateNode(opts, args):
239
  """Relocate all secondary instance from a node.
240

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

    
247
  """
248
  cl = GetClient()
249
  force = opts.force
250

    
251
  dst_node = opts.dst_node
252
  iallocator = opts.iallocator
253

    
254
  cnt = [dst_node, iallocator].count(None)
255
  if cnt != 1:
256
    raise errors.OpPrereqError("One and only one of the -n and -I"
257
                               " options must be passed", errors.ECODE_INVAL)
258

    
259
  selected_fields = ["name", "sinst_list"]
260
  src_node = args[0]
261

    
262
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
263
                         use_locking=False)
264
  src_node, sinst = result[0]
265

    
266
  if not sinst:
267
    ToStderr("No secondary instances on node %s, exiting.", src_node)
268
    return constants.EXIT_SUCCESS
269

    
270
  if dst_node is not None:
271
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
272
                           use_locking=False)
273
    dst_node = result[0][0]
274

    
275
    if src_node == dst_node:
276
      raise errors.OpPrereqError("Evacuate node needs different source and"
277
                                 " target nodes (node %s given twice)" %
278
                                 src_node, errors.ECODE_INVAL)
279
    txt_msg = "to node %s" % dst_node
280
  else:
281
    txt_msg = "using iallocator %s" % iallocator
282

    
283
  sinst = utils.NiceSort(sinst)
284

    
285
  if not force and not AskUser("Relocate instance(s) %s from node\n"
286
                               " %s %s?" %
287
                               (",".join("'%s'" % name for name in sinst),
288
                               src_node, txt_msg)):
289
    return constants.EXIT_CONFIRMATION
290

    
291
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
292
                              iallocator=iallocator)
293
  SubmitOpCode(op, cl=cl)
294

    
295

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

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

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

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

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

    
320
  pinst = utils.NiceSort(pinst)
321

    
322
  retcode = 0
323

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

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

    
342

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

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

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

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

    
358
  pinst = utils.NiceSort(pinst)
359

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

    
364
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
365
  SubmitOpCode(op, cl=cl)
366

    
367

    
368
def ShowNodeConfig(opts, args):
369
  """Show node information.
370

    
371
  @param opts: the command line options selected by the user
372
  @type args: list
373
  @param args: should either be an empty list, in which case
374
      we show information about all nodes, or should contain
375
      a list of nodes to be queried for information
376
  @rtype: int
377
  @return: the desired exit code
378

    
379
  """
380
  cl = GetClient()
381
  result = cl.QueryNodes(fields=["name", "pip", "sip",
382
                                 "pinst_list", "sinst_list",
383
                                 "master_candidate", "drained", "offline"],
384
                         names=args, use_locking=False)
385

    
386
  for (name, primary_ip, secondary_ip, pinst, sinst,
387
       is_mc, drained, offline) in result:
388
    ToStdout("Node name: %s", name)
389
    ToStdout("  primary ip: %s", primary_ip)
390
    ToStdout("  secondary ip: %s", secondary_ip)
391
    ToStdout("  master candidate: %s", is_mc)
392
    ToStdout("  drained: %s", drained)
393
    ToStdout("  offline: %s", offline)
394
    if pinst:
395
      ToStdout("  primary for instances:")
396
      for iname in utils.NiceSort(pinst):
397
        ToStdout("    - %s", iname)
398
    else:
399
      ToStdout("  primary for no instances")
400
    if sinst:
401
      ToStdout("  secondary for instances:")
402
      for iname in utils.NiceSort(sinst):
403
        ToStdout("    - %s", iname)
404
    else:
405
      ToStdout("  secondary for no instances")
406

    
407
  return 0
408

    
409

    
410
def RemoveNode(opts, args):
411
  """Remove a node from the cluster.
412

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

    
420
  """
421
  op = opcodes.OpRemoveNode(node_name=args[0])
422
  SubmitOpCode(op)
423
  return 0
424

    
425

    
426
def PowercycleNode(opts, args):
427
  """Remove a node from the cluster.
428

    
429
  @param opts: the command line options selected by the user
430
  @type args: list
431
  @param args: should contain only one element, the name of
432
      the node to be removed
433
  @rtype: int
434
  @return: the desired exit code
435

    
436
  """
437
  node = args[0]
438
  if (not opts.confirm and
439
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
440
    return 2
441

    
442
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
443
  result = SubmitOpCode(op)
444
  ToStderr(result)
445
  return 0
446

    
447

    
448
def ListVolumes(opts, args):
449
  """List logical volumes on node(s).
450

    
451
  @param opts: the command line options selected by the user
452
  @type args: list
453
  @param args: should either be an empty list, in which case
454
      we list data for all nodes, or contain a list of nodes
455
      to display data only for those
456
  @rtype: int
457
  @return: the desired exit code
458

    
459
  """
460
  if opts.output is None:
461
    selected_fields = ["node", "phys", "vg",
462
                       "name", "size", "instance"]
463
  else:
464
    selected_fields = opts.output.split(",")
465

    
466
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
467
  output = SubmitOpCode(op)
468

    
469
  if not opts.no_headers:
470
    headers = {"node": "Node", "phys": "PhysDev",
471
               "vg": "VG", "name": "Name",
472
               "size": "Size", "instance": "Instance"}
473
  else:
474
    headers = None
475

    
476
  unitfields = ["size"]
477

    
478
  numfields = ["size"]
479

    
480
  data = GenerateTable(separator=opts.separator, headers=headers,
481
                       fields=selected_fields, unitfields=unitfields,
482
                       numfields=numfields, data=output, units=opts.units)
483

    
484
  for line in data:
485
    ToStdout(line)
486

    
487
  return 0
488

    
489

    
490
def ListStorage(opts, args):
491
  """List physical volumes on node(s).
492

    
493
  @param opts: the command line options selected by the user
494
  @type args: list
495
  @param args: should either be an empty list, in which case
496
      we list data for all nodes, or contain a list of nodes
497
      to display data only for those
498
  @rtype: int
499
  @return: the desired exit code
500

    
501
  """
502
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
503
  if opts.user_storage_type is None:
504
    opts.user_storage_type = constants.ST_LVM_PV
505

    
506
  storage_type = ConvertStorageType(opts.user_storage_type)
507

    
508
  if opts.output is None:
509
    selected_fields = _LIST_STOR_DEF_FIELDS
510
  elif opts.output.startswith("+"):
511
    selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
512
  else:
513
    selected_fields = opts.output.split(",")
514

    
515
  op = opcodes.OpQueryNodeStorage(nodes=args,
516
                                  storage_type=storage_type,
517
                                  output_fields=selected_fields)
518
  output = SubmitOpCode(op)
519

    
520
  if not opts.no_headers:
521
    headers = {
522
      constants.SF_NODE: "Node",
523
      constants.SF_TYPE: "Type",
524
      constants.SF_NAME: "Name",
525
      constants.SF_SIZE: "Size",
526
      constants.SF_USED: "Used",
527
      constants.SF_FREE: "Free",
528
      constants.SF_ALLOCATABLE: "Allocatable",
529
      }
530
  else:
531
    headers = None
532

    
533
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
534
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
535

    
536
  # change raw values to nicer strings
537
  for row in output:
538
    for idx, field in enumerate(selected_fields):
539
      val = row[idx]
540
      if field == constants.SF_ALLOCATABLE:
541
        if val:
542
          val = "Y"
543
        else:
544
          val = "N"
545
      row[idx] = str(val)
546

    
547
  data = GenerateTable(separator=opts.separator, headers=headers,
548
                       fields=selected_fields, unitfields=unitfields,
549
                       numfields=numfields, data=output, units=opts.units)
550

    
551
  for line in data:
552
    ToStdout(line)
553

    
554
  return 0
555

    
556

    
557
def ModifyStorage(opts, args):
558
  """Modify storage volume on a node.
559

    
560
  @param opts: the command line options selected by the user
561
  @type args: list
562
  @param args: should contain 3 items: node name, storage type and volume name
563
  @rtype: int
564
  @return: the desired exit code
565

    
566
  """
567
  (node_name, user_storage_type, volume_name) = args
568

    
569
  storage_type = ConvertStorageType(user_storage_type)
570

    
571
  changes = {}
572

    
573
  if opts.allocatable is not None:
574
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
575

    
576
  if changes:
577
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
578
                                     storage_type=storage_type,
579
                                     name=volume_name,
580
                                     changes=changes)
581
    SubmitOpCode(op)
582
  else:
583
    ToStderr("No changes to perform, exiting.")
584

    
585

    
586
def RepairStorage(opts, args):
587
  """Repairs a storage volume on a node.
588

    
589
  @param opts: the command line options selected by the user
590
  @type args: list
591
  @param args: should contain 3 items: node name, storage type and volume name
592
  @rtype: int
593
  @return: the desired exit code
594

    
595
  """
596
  (node_name, user_storage_type, volume_name) = args
597

    
598
  storage_type = ConvertStorageType(user_storage_type)
599

    
600
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
601
                                   storage_type=storage_type,
602
                                   name=volume_name,
603
                                   ignore_consistency=opts.ignore_consistency)
604
  SubmitOpCode(op)
605

    
606

    
607
def SetNodeParams(opts, args):
608
  """Modifies a node.
609

    
610
  @param opts: the command line options selected by the user
611
  @type args: list
612
  @param args: should contain only one element, the node name
613
  @rtype: int
614
  @return: the desired exit code
615

    
616
  """
617
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
618
    ToStderr("Please give at least one of the parameters.")
619
    return 1
620

    
621
  if opts.master_candidate is not None:
622
    candidate = opts.master_candidate == 'yes'
623
  else:
624
    candidate = None
625
  if opts.offline is not None:
626
    offline = opts.offline == 'yes'
627
  else:
628
    offline = None
629

    
630
  if opts.drained is not None:
631
    drained = opts.drained == 'yes'
632
  else:
633
    drained = None
634
  op = opcodes.OpSetNodeParams(node_name=args[0],
635
                               master_candidate=candidate,
636
                               offline=offline,
637
                               drained=drained,
638
                               force=opts.force)
639

    
640
  # even if here we process the result, we allow submit only
641
  result = SubmitOrSend(op, opts)
642

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

    
649

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

    
729

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