Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 86f5eae3

History | View | Annotate | Download (22.5 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
  }
61

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

    
69

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

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

    
79

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

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

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

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

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

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

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

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

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

    
134

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

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

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

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

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

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

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

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

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

    
188
  return 0
189

    
190

    
191
def EvacuateNode(opts, args):
192
  """Relocate all secondary instance from a node.
193

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

    
200
  """
201
  cl = GetClient()
202
  force = opts.force
203

    
204
  dst_node = opts.dst_node
205
  iallocator = opts.iallocator
206

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

    
212
  selected_fields = ["name", "sinst_list"]
213
  src_node = args[0]
214

    
215
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
216
                         use_locking=False)
217
  src_node, sinst = result[0]
218

    
219
  if not sinst:
220
    ToStderr("No secondary instances on node %s, exiting.", src_node)
221
    return constants.EXIT_SUCCESS
222

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

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

    
236
  sinst = utils.NiceSort(sinst)
237

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

    
244
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
245
                              iallocator=iallocator)
246
  SubmitOpCode(op, cl=cl)
247

    
248

    
249
def FailoverNode(opts, args):
250
  """Failover all primary instance on a node.
251

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

    
258
  """
259
  cl = GetClient()
260
  force = opts.force
261
  selected_fields = ["name", "pinst_list"]
262

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

    
269
  if not pinst:
270
    ToStderr("No primary instances on node %s, exiting.", node)
271
    return 0
272

    
273
  pinst = utils.NiceSort(pinst)
274

    
275
  retcode = 0
276

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

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

    
295

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

    
299
  """
300
  cl = GetClient()
301
  force = opts.force
302
  selected_fields = ["name", "pinst_list"]
303

    
304
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
305
  node, pinst = result[0]
306

    
307
  if not pinst:
308
    ToStdout("No primary instances on node %s, exiting." % node)
309
    return 0
310

    
311
  pinst = utils.NiceSort(pinst)
312

    
313
  retcode = 0
314

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

    
319
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
320
  SubmitOpCode(op, cl=cl)
321

    
322

    
323
def ShowNodeConfig(opts, args):
324
  """Show node information.
325

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

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

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

    
362
  return 0
363

    
364

    
365
def RemoveNode(opts, args):
366
  """Remove a node from the cluster.
367

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

    
375
  """
376
  op = opcodes.OpRemoveNode(node_name=args[0])
377
  SubmitOpCode(op)
378
  return 0
379

    
380

    
381
def PowercycleNode(opts, args):
382
  """Remove a node from the cluster.
383

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

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

    
397
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
398
  result = SubmitOpCode(op)
399
  ToStderr(result)
400
  return 0
401

    
402

    
403
def ListVolumes(opts, args):
404
  """List logical volumes on node(s).
405

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

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

    
421
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
422
  output = SubmitOpCode(op)
423

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

    
431
  unitfields = ["size"]
432

    
433
  numfields = ["size"]
434

    
435
  data = GenerateTable(separator=opts.separator, headers=headers,
436
                       fields=selected_fields, unitfields=unitfields,
437
                       numfields=numfields, data=output, units=opts.units)
438

    
439
  for line in data:
440
    ToStdout(line)
441

    
442
  return 0
443

    
444

    
445
def ListPhysicalVolumes(opts, args):
446
  """List physical volumes on node(s).
447

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

    
456
  """
457
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
458
  if opts.user_storage_type is None:
459
    opts.user_storage_type = constants.ST_LVM_PV
460

    
461
  storage_type = ConvertStorageType(opts.user_storage_type)
462

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

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

    
487
  op = opcodes.OpQueryNodeStorage(nodes=args,
488
                                  storage_type=storage_type,
489
                                  output_fields=selected_fields)
490
  output = SubmitOpCode(op)
491

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

    
504
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
505
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
506

    
507
  data = GenerateTable(separator=opts.separator, headers=headers,
508
                       fields=selected_fields, unitfields=unitfields,
509
                       numfields=numfields, data=output, units=opts.units)
510

    
511
  for line in data:
512
    ToStdout(line)
513

    
514
  return 0
515

    
516

    
517
def ModifyVolume(opts, args):
518
  """Modify storage volume on a node.
519

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

    
526
  """
527
  (node_name, user_storage_type, volume_name) = args
528

    
529
  storage_type = ConvertStorageType(user_storage_type)
530

    
531
  changes = {}
532

    
533
  if opts.allocatable is not None:
534
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
535

    
536
  if changes:
537
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
538
                                     storage_type=storage_type,
539
                                     name=volume_name,
540
                                     changes=changes)
541
    SubmitOpCode(op)
542

    
543

    
544
def SetNodeParams(opts, args):
545
  """Modifies a node.
546

    
547
  @param opts: the command line options selected by the user
548
  @type args: list
549
  @param args: should contain only one element, the node name
550
  @rtype: int
551
  @return: the desired exit code
552

    
553
  """
554
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
555
    ToStderr("Please give at least one of the parameters.")
556
    return 1
557

    
558
  if opts.master_candidate is not None:
559
    candidate = opts.master_candidate == 'yes'
560
  else:
561
    candidate = None
562
  if opts.offline is not None:
563
    offline = opts.offline == 'yes'
564
  else:
565
    offline = None
566

    
567
  if opts.drained is not None:
568
    drained = opts.drained == 'yes'
569
  else:
570
    drained = None
571
  op = opcodes.OpSetNodeParams(node_name=args[0],
572
                               master_candidate=candidate,
573
                               offline=offline,
574
                               drained=drained,
575
                               force=opts.force)
576

    
577
  # even if here we process the result, we allow submit only
578
  result = SubmitOrSend(op, opts)
579

    
580
  if result:
581
    ToStdout("Modified node %s", args[0])
582
    for param, data in result:
583
      ToStdout(" - %-5s -> %s", param, data)
584
  return 0
585

    
586

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

    
699

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