Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 90f72445

History | View | Annotate | Download (23.4 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

    
22
# pylint: disable-msg=W0401,W0614
23
# W0401: Wildcard import ganeti.cli
24
# W0614: Unused import %s from wildcard import (since we need cli)
25

    
26
import sys
27
from optparse import make_option
28

    
29
from ganeti.cli import *
30
from ganeti import cli
31
from ganeti import opcodes
32
from ganeti import utils
33
from ganeti import constants
34
from ganeti import errors
35
from ganeti import bootstrap
36

    
37

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

    
45
#: headers (and full field list for L{ListNodes}
46
_LIST_HEADERS = {
47
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
48
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
49
  "pip": "PrimaryIP", "sip": "SecondaryIP",
50
  "dtotal": "DTotal", "dfree": "DFree",
51
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
52
  "bootid": "BootID",
53
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
54
  "tags": "Tags",
55
  "serial_no": "SerialNo",
56
  "master_candidate": "MasterC",
57
  "master": "IsMaster",
58
  "offline": "Offline", "drained": "Drained",
59
  "role": "Role",
60
  "ctime": "CTime", "mtime": "MTime",
61
  }
62

    
63
#: User-facing storage unit types
64
_USER_STORAGE_TYPE = {
65
  constants.ST_FILE: "file",
66
  constants.ST_LVM_PV: "lvm-pv",
67
  constants.ST_LVM_VG: "lvm-vg",
68
  }
69

    
70

    
71
def ConvertStorageType(user_storage_type):
72
  """Converts a user storage type to its internal name.
73

    
74
  """
75
  try:
76
    return _USER_STORAGE_TYPE[user_storage_type]
77
  except KeyError:
78
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
79

    
80

    
81
@UsesRPC
82
def AddNode(opts, args):
83
  """Add a node to the cluster.
84

    
85
  @param opts: the command line options selected by the user
86
  @type args: list
87
  @param args: should contain only one element, the new node name
88
  @rtype: int
89
  @return: the desired exit code
90

    
91
  """
92
  cl = GetClient()
93
  dns_data = utils.HostInfo(args[0])
94
  node = dns_data.name
95
  readd = opts.readd
96

    
97
  try:
98
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
99
                           use_locking=False)
100
    node_exists, sip = output[0]
101
  except (errors.OpPrereqError, errors.OpExecError):
102
    node_exists = ""
103
    sip = None
104

    
105
  if readd:
106
    if not node_exists:
107
      ToStderr("Node %s not in the cluster"
108
               " - please retry without '--readd'", node)
109
      return 1
110
  else:
111
    if node_exists:
112
      ToStderr("Node %s already in the cluster (as %s)"
113
               " - please retry with '--readd'", node, node_exists)
114
      return 1
115
    sip = opts.secondary_ip
116

    
117
  # read the cluster name from the master
118
  output = cl.QueryConfigValues(['cluster_name'])
119
  cluster_name = output[0]
120

    
121
  if not readd:
122
    ToStderr("-- WARNING -- \n"
123
             "Performing this operation is going to replace the ssh daemon"
124
             " keypair\n"
125
             "on the target machine (%s) with the ones of the"
126
             " current one\n"
127
             "and grant full intra-cluster ssh root access to/from it\n", node)
128

    
129
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
130

    
131
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
132
                         readd=opts.readd)
133
  SubmitOpCode(op)
134

    
135

    
136
def ListNodes(opts, args):
137
  """List nodes and their properties.
138

    
139
  @param opts: the command line options selected by the user
140
  @type args: list
141
  @param args: should be an empty list
142
  @rtype: int
143
  @return: the desired exit code
144

    
145
  """
146
  if opts.output is None:
147
    selected_fields = _LIST_DEF_FIELDS
148
  elif opts.output.startswith("+"):
149
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
150
  else:
151
    selected_fields = opts.output.split(",")
152

    
153
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
154

    
155
  if not opts.no_headers:
156
    headers = _LIST_HEADERS
157
  else:
158
    headers = None
159

    
160
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
161

    
162
  numfields = ["dtotal", "dfree",
163
               "mtotal", "mnode", "mfree",
164
               "pinst_cnt", "sinst_cnt",
165
               "ctotal", "serial_no"]
166

    
167
  list_type_fields = ("pinst_list", "sinst_list", "tags")
168
  # change raw values to nicer strings
169
  for row in output:
170
    for idx, field in enumerate(selected_fields):
171
      val = row[idx]
172
      if field in list_type_fields:
173
        val = ",".join(val)
174
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
175
        if val:
176
          val = 'Y'
177
        else:
178
          val = 'N'
179
      elif field == "ctime" or field == "mtime":
180
        val = utils.FormatTime(val)
181
      elif val is None:
182
        val = "?"
183
      row[idx] = str(val)
184

    
185
  data = GenerateTable(separator=opts.separator, headers=headers,
186
                       fields=selected_fields, unitfields=unitfields,
187
                       numfields=numfields, data=output, units=opts.units)
188
  for line in data:
189
    ToStdout(line)
190

    
191
  return 0
192

    
193

    
194
def EvacuateNode(opts, args):
195
  """Relocate all secondary instance from a node.
196

    
197
  @param opts: the command line options selected by the user
198
  @type args: list
199
  @param args: should be an empty list
200
  @rtype: int
201
  @return: the desired exit code
202

    
203
  """
204
  cl = GetClient()
205
  force = opts.force
206

    
207
  dst_node = opts.dst_node
208
  iallocator = opts.iallocator
209

    
210
  cnt = [dst_node, iallocator].count(None)
211
  if cnt != 1:
212
    raise errors.OpPrereqError("One and only one of the -n and -i"
213
                               " options must be passed")
214

    
215
  selected_fields = ["name", "sinst_list"]
216
  src_node = args[0]
217

    
218
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
219
                         use_locking=False)
220
  src_node, sinst = result[0]
221

    
222
  if not sinst:
223
    ToStderr("No secondary instances on node %s, exiting.", src_node)
224
    return constants.EXIT_SUCCESS
225

    
226
  if dst_node is not None:
227
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
228
                           use_locking=False)
229
    dst_node = result[0][0]
230

    
231
    if src_node == dst_node:
232
      raise errors.OpPrereqError("Evacuate node needs different source and"
233
                                 " target nodes (node %s given twice)" %
234
                                 src_node)
235
    txt_msg = "to node %s" % dst_node
236
  else:
237
    txt_msg = "using iallocator %s" % iallocator
238

    
239
  sinst = utils.NiceSort(sinst)
240

    
241
  if not force and not AskUser("Relocate instance(s) %s from node\n"
242
                               " %s %s?" %
243
                               (",".join("'%s'" % name for name in sinst),
244
                               src_node, txt_msg)):
245
    return constants.EXIT_CONFIRMATION
246

    
247
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
248
                              iallocator=iallocator)
249
  SubmitOpCode(op, cl=cl)
250

    
251

    
252
def FailoverNode(opts, args):
253
  """Failover all primary instance on a node.
254

    
255
  @param opts: the command line options selected by the user
256
  @type args: list
257
  @param args: should be an empty list
258
  @rtype: int
259
  @return: the desired exit code
260

    
261
  """
262
  cl = GetClient()
263
  force = opts.force
264
  selected_fields = ["name", "pinst_list"]
265

    
266
  # these fields are static data anyway, so it doesn't matter, but
267
  # locking=True should be safer
268
  result = cl.QueryNodes(names=args, fields=selected_fields,
269
                         use_locking=False)
270
  node, pinst = result[0]
271

    
272
  if not pinst:
273
    ToStderr("No primary instances on node %s, exiting.", node)
274
    return 0
275

    
276
  pinst = utils.NiceSort(pinst)
277

    
278
  retcode = 0
279

    
280
  if not force and not AskUser("Fail over instance(s) %s?" %
281
                               (",".join("'%s'" % name for name in pinst))):
282
    return 2
283

    
284
  jex = JobExecutor(cl=cl)
285
  for iname in pinst:
286
    op = opcodes.OpFailoverInstance(instance_name=iname,
287
                                    ignore_consistency=opts.ignore_consistency)
288
    jex.QueueJob(iname, op)
289
  results = jex.GetResults()
290
  bad_cnt = len([row for row in results if not row[0]])
291
  if bad_cnt == 0:
292
    ToStdout("All %d instance(s) failed over successfully.", len(results))
293
  else:
294
    ToStdout("There were errors during the failover:\n"
295
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
296
  return retcode
297

    
298

    
299
def MigrateNode(opts, args):
300
  """Migrate all primary instance on a node.
301

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

    
307
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
308
  node, pinst = result[0]
309

    
310
  if not pinst:
311
    ToStdout("No primary instances on node %s, exiting." % node)
312
    return 0
313

    
314
  pinst = utils.NiceSort(pinst)
315

    
316
  retcode = 0
317

    
318
  if not force and not AskUser("Migrate instance(s) %s?" %
319
                               (",".join("'%s'" % name for name in pinst))):
320
    return 2
321

    
322
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
323
  SubmitOpCode(op, cl=cl)
324

    
325

    
326
def ShowNodeConfig(opts, args):
327
  """Show node information.
328

    
329
  @param opts: the command line options selected by the user
330
  @type args: list
331
  @param args: should either be an empty list, in which case
332
      we show information about all nodes, or should contain
333
      a list of nodes to be queried for information
334
  @rtype: int
335
  @return: the desired exit code
336

    
337
  """
338
  cl = GetClient()
339
  result = cl.QueryNodes(fields=["name", "pip", "sip",
340
                                 "pinst_list", "sinst_list",
341
                                 "master_candidate", "drained", "offline"],
342
                         names=args, use_locking=False)
343

    
344
  for (name, primary_ip, secondary_ip, pinst, sinst,
345
       is_mc, drained, offline) in result:
346
    ToStdout("Node name: %s", name)
347
    ToStdout("  primary ip: %s", primary_ip)
348
    ToStdout("  secondary ip: %s", secondary_ip)
349
    ToStdout("  master candidate: %s", is_mc)
350
    ToStdout("  drained: %s", drained)
351
    ToStdout("  offline: %s", offline)
352
    if pinst:
353
      ToStdout("  primary for instances:")
354
      for iname in utils.NiceSort(pinst):
355
        ToStdout("    - %s", iname)
356
    else:
357
      ToStdout("  primary for no instances")
358
    if sinst:
359
      ToStdout("  secondary for instances:")
360
      for iname in utils.NiceSort(sinst):
361
        ToStdout("    - %s", iname)
362
    else:
363
      ToStdout("  secondary for no instances")
364

    
365
  return 0
366

    
367

    
368
def RemoveNode(opts, args):
369
  """Remove a node from the cluster.
370

    
371
  @param opts: the command line options selected by the user
372
  @type args: list
373
  @param args: should contain only one element, the name of
374
      the node to be removed
375
  @rtype: int
376
  @return: the desired exit code
377

    
378
  """
379
  op = opcodes.OpRemoveNode(node_name=args[0])
380
  SubmitOpCode(op)
381
  return 0
382

    
383

    
384
def PowercycleNode(opts, args):
385
  """Remove a node from the cluster.
386

    
387
  @param opts: the command line options selected by the user
388
  @type args: list
389
  @param args: should contain only one element, the name of
390
      the node to be removed
391
  @rtype: int
392
  @return: the desired exit code
393

    
394
  """
395
  node = args[0]
396
  if (not opts.confirm and
397
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
398
    return 2
399

    
400
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
401
  result = SubmitOpCode(op)
402
  ToStderr(result)
403
  return 0
404

    
405

    
406
def ListVolumes(opts, args):
407
  """List logical volumes on node(s).
408

    
409
  @param opts: the command line options selected by the user
410
  @type args: list
411
  @param args: should either be an empty list, in which case
412
      we list data for all nodes, or contain a list of nodes
413
      to display data only for those
414
  @rtype: int
415
  @return: the desired exit code
416

    
417
  """
418
  if opts.output is None:
419
    selected_fields = ["node", "phys", "vg",
420
                       "name", "size", "instance"]
421
  else:
422
    selected_fields = opts.output.split(",")
423

    
424
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
425
  output = SubmitOpCode(op)
426

    
427
  if not opts.no_headers:
428
    headers = {"node": "Node", "phys": "PhysDev",
429
               "vg": "VG", "name": "Name",
430
               "size": "Size", "instance": "Instance"}
431
  else:
432
    headers = None
433

    
434
  unitfields = ["size"]
435

    
436
  numfields = ["size"]
437

    
438
  data = GenerateTable(separator=opts.separator, headers=headers,
439
                       fields=selected_fields, unitfields=unitfields,
440
                       numfields=numfields, data=output, units=opts.units)
441

    
442
  for line in data:
443
    ToStdout(line)
444

    
445
  return 0
446

    
447

    
448
def ListPhysicalVolumes(opts, args):
449
  """List physical 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
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
461
  if opts.user_storage_type is None:
462
    opts.user_storage_type = constants.ST_LVM_PV
463

    
464
  storage_type = ConvertStorageType(opts.user_storage_type)
465

    
466
  default_fields = {
467
    constants.ST_FILE: [
468
      constants.SF_NAME,
469
      constants.SF_USED,
470
      constants.SF_FREE,
471
      ],
472
    constants.ST_LVM_PV: [
473
      constants.SF_NAME,
474
      constants.SF_SIZE,
475
      constants.SF_USED,
476
      constants.SF_FREE,
477
      ],
478
    constants.ST_LVM_VG: [
479
      constants.SF_NAME,
480
      constants.SF_SIZE,
481
      ],
482
  }
483

    
484
  if opts.output is None:
485
    selected_fields = ["node"]
486
    selected_fields.extend(default_fields[storage_type])
487
  else:
488
    selected_fields = opts.output.split(",")
489

    
490
  op = opcodes.OpQueryNodeStorage(nodes=args,
491
                                  storage_type=storage_type,
492
                                  output_fields=selected_fields)
493
  output = SubmitOpCode(op)
494

    
495
  if not opts.no_headers:
496
    headers = {
497
      "node": "Node",
498
      constants.SF_NAME: "Name",
499
      constants.SF_SIZE: "Size",
500
      constants.SF_USED: "Used",
501
      constants.SF_FREE: "Free",
502
      constants.SF_ALLOCATABLE: "Allocatable",
503
      }
504
  else:
505
    headers = None
506

    
507
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
508
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
509

    
510
  data = GenerateTable(separator=opts.separator, headers=headers,
511
                       fields=selected_fields, unitfields=unitfields,
512
                       numfields=numfields, data=output, units=opts.units)
513

    
514
  for line in data:
515
    ToStdout(line)
516

    
517
  return 0
518

    
519

    
520
def ModifyVolume(opts, args):
521
  """Modify storage volume on a node.
522

    
523
  @param opts: the command line options selected by the user
524
  @type args: list
525
  @param args: should contain 3 items: node name, storage type and volume name
526
  @rtype: int
527
  @return: the desired exit code
528

    
529
  """
530
  (node_name, user_storage_type, volume_name) = args
531

    
532
  storage_type = ConvertStorageType(user_storage_type)
533

    
534
  changes = {}
535

    
536
  if opts.allocatable is not None:
537
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
538

    
539
  if changes:
540
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
541
                                     storage_type=storage_type,
542
                                     name=volume_name,
543
                                     changes=changes)
544
    SubmitOpCode(op)
545

    
546

    
547
def RepairVolume(opts, args):
548
  """Repairs a storage volume on a node.
549

    
550
  @param opts: the command line options selected by the user
551
  @type args: list
552
  @param args: should contain 3 items: node name, storage type and volume name
553
  @rtype: int
554
  @return: the desired exit code
555

    
556
  """
557
  (node_name, user_storage_type, volume_name) = args
558

    
559
  storage_type = ConvertStorageType(user_storage_type)
560

    
561
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
562
                                   storage_type=storage_type,
563
                                   name=volume_name)
564
  SubmitOpCode(op)
565

    
566

    
567
def SetNodeParams(opts, args):
568
  """Modifies a node.
569

    
570
  @param opts: the command line options selected by the user
571
  @type args: list
572
  @param args: should contain only one element, the node name
573
  @rtype: int
574
  @return: the desired exit code
575

    
576
  """
577
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
578
    ToStderr("Please give at least one of the parameters.")
579
    return 1
580

    
581
  if opts.master_candidate is not None:
582
    candidate = opts.master_candidate == 'yes'
583
  else:
584
    candidate = None
585
  if opts.offline is not None:
586
    offline = opts.offline == 'yes'
587
  else:
588
    offline = None
589

    
590
  if opts.drained is not None:
591
    drained = opts.drained == 'yes'
592
  else:
593
    drained = None
594
  op = opcodes.OpSetNodeParams(node_name=args[0],
595
                               master_candidate=candidate,
596
                               offline=offline,
597
                               drained=drained,
598
                               force=opts.force)
599

    
600
  # even if here we process the result, we allow submit only
601
  result = SubmitOrSend(op, opts)
602

    
603
  if result:
604
    ToStdout("Modified node %s", args[0])
605
    for param, data in result:
606
      ToStdout(" - %-5s -> %s", param, data)
607
  return 0
608

    
609

    
610
commands = {
611
  'add': (AddNode, ARGS_ONE,
612
          [DEBUG_OPT,
613
           make_option("-s", "--secondary-ip", dest="secondary_ip",
614
                       help="Specify the secondary ip for the node",
615
                       metavar="ADDRESS", default=None),
616
           make_option("--readd", dest="readd",
617
                       default=False, action="store_true",
618
                       help="Readd old node after replacing it"),
619
           make_option("--no-ssh-key-check", dest="ssh_key_check",
620
                       default=True, action="store_false",
621
                       help="Disable SSH key fingerprint checking"),
622
           ],
623
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
624
          "Add a node to the cluster"),
625
  'evacuate': (EvacuateNode, ARGS_ONE,
626
               [DEBUG_OPT, FORCE_OPT,
627
                make_option("-n", "--new-secondary", dest="dst_node",
628
                            help="New secondary node", metavar="NODE",
629
                            default=None),
630
                make_option("-I", "--iallocator", metavar="<NAME>",
631
                            help="Select new secondary for the instance"
632
                            " automatically using the"
633
                            " <NAME> iallocator plugin",
634
                            default=None, type="string"),
635
                ],
636
               "[-f] {-I <iallocator> | -n <dst>} <node>",
637
               "Relocate the secondary instances from a node"
638
               " to other nodes (only for instances with drbd disk template)"),
639
  'failover': (FailoverNode, ARGS_ONE,
640
               [DEBUG_OPT, FORCE_OPT,
641
                make_option("--ignore-consistency", dest="ignore_consistency",
642
                            action="store_true", default=False,
643
                            help="Ignore the consistency of the disks on"
644
                            " the secondary"),
645
                ],
646
               "[-f] <node>",
647
               "Stops the primary instances on a node and start them on their"
648
               " secondary node (only for instances with drbd disk template)"),
649
  'migrate': (MigrateNode, ARGS_ONE,
650
               [DEBUG_OPT, FORCE_OPT,
651
                make_option("--non-live", dest="live",
652
                            default=True, action="store_false",
653
                            help="Do a non-live migration (this usually means"
654
                            " freeze the instance, save the state,"
655
                            " transfer and only then resume running on the"
656
                            " secondary node)"),
657
                ],
658
               "[-f] <node>",
659
               "Migrate all the primary instance on a node away from it"
660
               " (only for instances of type drbd)"),
661
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
662
           "[<node_name>...]", "Show information about the node(s)"),
663
  'list': (ListNodes, ARGS_ANY,
664
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
665
           "[nodes...]",
666
           "Lists the nodes in the cluster. The available fields"
667
           " are (see the man page for details): %s"
668
           " The default field list is (in order): %s." %
669
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
670
  'modify': (SetNodeParams, ARGS_ONE,
671
             [DEBUG_OPT, FORCE_OPT,
672
              SUBMIT_OPT,
673
              make_option("-C", "--master-candidate", dest="master_candidate",
674
                          choices=('yes', 'no'), default=None,
675
                          metavar="yes|no",
676
                          help="Set the master_candidate flag on the node"),
677
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
678
                          choices=('yes', 'no'), default=None,
679
                          help="Set the offline flag on the node"),
680
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
681
                          choices=('yes', 'no'), default=None,
682
                          help="Set the drained flag on the node"),
683
              ],
684
             "<instance>", "Alters the parameters of an instance"),
685
  'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
686
                 "<node_name>", "Tries to forcefully powercycle a node"),
687
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
688
             "<node_name>", "Removes a node from the cluster"),
689
  'volumes': (ListVolumes, ARGS_ANY,
690
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
691
              "[<node_name>...]", "List logical volumes on node(s)"),
692
  'physical-volumes': (ListPhysicalVolumes, ARGS_ANY,
693
                       [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
694
                        FIELDS_OPT,
695
                        make_option("--storage-type",
696
                                    dest="user_storage_type",
697
                                    choices=_USER_STORAGE_TYPE.keys(),
698
                                    default=None,
699
                                    metavar="STORAGE_TYPE",
700
                                    help=("Storage type (%s)" %
701
                                          utils.CommaJoin(_USER_STORAGE_TYPE.keys()))),
702
                       ],
703
                       "[<node_name>...]",
704
                       "List physical volumes on node(s)"),
705
  'modify-volume': (ModifyVolume, ARGS_FIXED(3),
706
                    [DEBUG_OPT,
707
                     make_option("--allocatable", dest="allocatable",
708
                                 choices=["yes", "no"], default=None,
709
                                 metavar="yes|no",
710
                                 help="Set the allocatable flag on a volume"),
711
                     ],
712
                    "<node_name> <storage_type> <name>",
713
                    "Modify storage volume on a node"),
714
  'repair-volume': (RepairVolume, ARGS_FIXED(3),
715
                    [DEBUG_OPT],
716
                    "<node_name> <storage_type> <name>",
717
                    "Repairs a storage volume on a node"),
718
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
719
                "<node_name>", "List the tags of the given node"),
720
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
721
               "<node_name> tag...", "Add tags to the given node"),
722
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
723
                  "<node_name> tag...", "Remove tags from the given node"),
724
  }
725

    
726

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