Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 53548798

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
@UsesRPC
71
def AddNode(opts, args):
72
  """Add a node to the cluster.
73

    
74
  @param opts: the command line options selected by the user
75
  @type args: list
76
  @param args: should contain only one element, the new node name
77
  @rtype: int
78
  @return: the desired exit code
79

    
80
  """
81
  cl = GetClient()
82
  dns_data = utils.HostInfo(args[0])
83
  node = dns_data.name
84
  readd = opts.readd
85

    
86
  try:
87
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
88
                           use_locking=False)
89
    node_exists, sip = output[0]
90
  except (errors.OpPrereqError, errors.OpExecError):
91
    node_exists = ""
92
    sip = None
93

    
94
  if readd:
95
    if not node_exists:
96
      ToStderr("Node %s not in the cluster"
97
               " - please retry without '--readd'", node)
98
      return 1
99
  else:
100
    if node_exists:
101
      ToStderr("Node %s already in the cluster (as %s)"
102
               " - please retry with '--readd'", node, node_exists)
103
      return 1
104
    sip = opts.secondary_ip
105

    
106
  # read the cluster name from the master
107
  output = cl.QueryConfigValues(['cluster_name'])
108
  cluster_name = output[0]
109

    
110
  if not readd:
111
    ToStderr("-- WARNING -- \n"
112
             "Performing this operation is going to replace the ssh daemon"
113
             " keypair\n"
114
             "on the target machine (%s) with the ones of the"
115
             " current one\n"
116
             "and grant full intra-cluster ssh root access to/from it\n", node)
117

    
118
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
119

    
120
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
121
                         readd=opts.readd)
122
  SubmitOpCode(op)
123

    
124

    
125
def ListNodes(opts, args):
126
  """List nodes and their properties.
127

    
128
  @param opts: the command line options selected by the user
129
  @type args: list
130
  @param args: should be an empty list
131
  @rtype: int
132
  @return: the desired exit code
133

    
134
  """
135
  if opts.output is None:
136
    selected_fields = _LIST_DEF_FIELDS
137
  elif opts.output.startswith("+"):
138
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
139
  else:
140
    selected_fields = opts.output.split(",")
141

    
142
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
143

    
144
  if not opts.no_headers:
145
    headers = _LIST_HEADERS
146
  else:
147
    headers = None
148

    
149
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
150

    
151
  numfields = ["dtotal", "dfree",
152
               "mtotal", "mnode", "mfree",
153
               "pinst_cnt", "sinst_cnt",
154
               "ctotal", "serial_no"]
155

    
156
  list_type_fields = ("pinst_list", "sinst_list", "tags")
157
  # change raw values to nicer strings
158
  for row in output:
159
    for idx, field in enumerate(selected_fields):
160
      val = row[idx]
161
      if field in list_type_fields:
162
        val = ",".join(val)
163
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
164
        if val:
165
          val = 'Y'
166
        else:
167
          val = 'N'
168
      elif val is None:
169
        val = "?"
170
      row[idx] = str(val)
171

    
172
  data = GenerateTable(separator=opts.separator, headers=headers,
173
                       fields=selected_fields, unitfields=unitfields,
174
                       numfields=numfields, data=output, units=opts.units)
175
  for line in data:
176
    ToStdout(line)
177

    
178
  return 0
179

    
180

    
181
def EvacuateNode(opts, args):
182
  """Relocate all secondary instance from a node.
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
  cl = GetClient()
192
  force = opts.force
193

    
194
  dst_node = opts.dst_node
195
  iallocator = opts.iallocator
196

    
197
  cnt = [dst_node, iallocator].count(None)
198
  if cnt != 1:
199
    raise errors.OpPrereqError("One and only one of the -n and -i"
200
                               " options must be passed")
201

    
202
  selected_fields = ["name", "sinst_list"]
203
  src_node = args[0]
204

    
205
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
206
                         use_locking=False)
207
  src_node, sinst = result[0]
208

    
209
  if not sinst:
210
    ToStderr("No secondary instances on node %s, exiting.", src_node)
211
    return constants.EXIT_SUCCESS
212

    
213
  if dst_node is not None:
214
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
215
                           use_locking=False)
216
    dst_node = result[0][0]
217

    
218
    if src_node == dst_node:
219
      raise errors.OpPrereqError("Evacuate node needs different source and"
220
                                 " target nodes (node %s given twice)" %
221
                                 src_node)
222
    txt_msg = "to node %s" % dst_node
223
  else:
224
    txt_msg = "using iallocator %s" % iallocator
225

    
226
  sinst = utils.NiceSort(sinst)
227

    
228
  if not force and not AskUser("Relocate instance(s) %s from node\n"
229
                               " %s %s?" %
230
                               (",".join("'%s'" % name for name in sinst),
231
                               src_node, txt_msg)):
232
    return constants.EXIT_CONFIRMATION
233

    
234
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
235
                              iallocator=iallocator)
236
  SubmitOpCode(op, cl=cl)
237

    
238

    
239
def FailoverNode(opts, args):
240
  """Failover all primary instance on a node.
241

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

    
248
  """
249
  cl = GetClient()
250
  force = opts.force
251
  selected_fields = ["name", "pinst_list"]
252

    
253
  # these fields are static data anyway, so it doesn't matter, but
254
  # locking=True should be safer
255
  result = cl.QueryNodes(names=args, fields=selected_fields,
256
                         use_locking=False)
257
  node, pinst = result[0]
258

    
259
  if not pinst:
260
    ToStderr("No primary instances on node %s, exiting.", node)
261
    return 0
262

    
263
  pinst = utils.NiceSort(pinst)
264

    
265
  retcode = 0
266

    
267
  if not force and not AskUser("Fail over instance(s) %s?" %
268
                               (",".join("'%s'" % name for name in pinst))):
269
    return 2
270

    
271
  jex = JobExecutor(cl=cl)
272
  for iname in pinst:
273
    op = opcodes.OpFailoverInstance(instance_name=iname,
274
                                    ignore_consistency=opts.ignore_consistency)
275
    jex.QueueJob(iname, op)
276
  results = jex.GetResults()
277
  bad_cnt = len([row for row in results if not row[0]])
278
  if bad_cnt == 0:
279
    ToStdout("All %d instance(s) failed over successfully.", len(results))
280
  else:
281
    ToStdout("There were errors during the failover:\n"
282
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
283
  return retcode
284

    
285

    
286
def MigrateNode(opts, args):
287
  """Migrate all primary instance on a node.
288

    
289
  """
290
  cl = GetClient()
291
  force = opts.force
292
  selected_fields = ["name", "pinst_list"]
293

    
294
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
295
  node, pinst = result[0]
296

    
297
  if not pinst:
298
    ToStdout("No primary instances on node %s, exiting." % node)
299
    return 0
300

    
301
  pinst = utils.NiceSort(pinst)
302

    
303
  retcode = 0
304

    
305
  if not force and not AskUser("Migrate instance(s) %s?" %
306
                               (",".join("'%s'" % name for name in pinst))):
307
    return 2
308

    
309
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
310
  SubmitOpCode(op, cl=cl)
311

    
312

    
313
def ShowNodeConfig(opts, args):
314
  """Show node information.
315

    
316
  @param opts: the command line options selected by the user
317
  @type args: list
318
  @param args: should either be an empty list, in which case
319
      we show information about all nodes, or should contain
320
      a list of nodes to be queried for information
321
  @rtype: int
322
  @return: the desired exit code
323

    
324
  """
325
  cl = GetClient()
326
  result = cl.QueryNodes(fields=["name", "pip", "sip",
327
                                 "pinst_list", "sinst_list",
328
                                 "master_candidate", "drained", "offline"],
329
                         names=args, use_locking=False)
330

    
331
  for (name, primary_ip, secondary_ip, pinst, sinst,
332
       is_mc, drained, offline) in result:
333
    ToStdout("Node name: %s", name)
334
    ToStdout("  primary ip: %s", primary_ip)
335
    ToStdout("  secondary ip: %s", secondary_ip)
336
    ToStdout("  master candidate: %s", is_mc)
337
    ToStdout("  drained: %s", drained)
338
    ToStdout("  offline: %s", offline)
339
    if pinst:
340
      ToStdout("  primary for instances:")
341
      for iname in utils.NiceSort(pinst):
342
        ToStdout("    - %s", iname)
343
    else:
344
      ToStdout("  primary for no instances")
345
    if sinst:
346
      ToStdout("  secondary for instances:")
347
      for iname in utils.NiceSort(sinst):
348
        ToStdout("    - %s", iname)
349
    else:
350
      ToStdout("  secondary for no instances")
351

    
352
  return 0
353

    
354

    
355
def RemoveNode(opts, args):
356
  """Remove a node from the cluster.
357

    
358
  @param opts: the command line options selected by the user
359
  @type args: list
360
  @param args: should contain only one element, the name of
361
      the node to be removed
362
  @rtype: int
363
  @return: the desired exit code
364

    
365
  """
366
  op = opcodes.OpRemoveNode(node_name=args[0])
367
  SubmitOpCode(op)
368
  return 0
369

    
370

    
371
def PowercycleNode(opts, args):
372
  """Remove a node from the cluster.
373

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

    
381
  """
382
  node = args[0]
383
  if (not opts.confirm and
384
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
385
    return 2
386

    
387
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
388
  result = SubmitOpCode(op)
389
  ToStderr(result)
390
  return 0
391

    
392

    
393
def ListVolumes(opts, args):
394
  """List logical volumes on node(s).
395

    
396
  @param opts: the command line options selected by the user
397
  @type args: list
398
  @param args: should either be an empty list, in which case
399
      we list data for all nodes, or contain a list of nodes
400
      to display data only for those
401
  @rtype: int
402
  @return: the desired exit code
403

    
404
  """
405
  if opts.output is None:
406
    selected_fields = ["node", "phys", "vg",
407
                       "name", "size", "instance"]
408
  else:
409
    selected_fields = opts.output.split(",")
410

    
411
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
412
  output = SubmitOpCode(op)
413

    
414
  if not opts.no_headers:
415
    headers = {"node": "Node", "phys": "PhysDev",
416
               "vg": "VG", "name": "Name",
417
               "size": "Size", "instance": "Instance"}
418
  else:
419
    headers = None
420

    
421
  unitfields = ["size"]
422

    
423
  numfields = ["size"]
424

    
425
  data = GenerateTable(separator=opts.separator, headers=headers,
426
                       fields=selected_fields, unitfields=unitfields,
427
                       numfields=numfields, data=output, units=opts.units)
428

    
429
  for line in data:
430
    ToStdout(line)
431

    
432
  return 0
433

    
434

    
435
def ListPhysicalVolumes(opts, args):
436
  """List physical volumes on node(s).
437

    
438
  @param opts: the command line options selected by the user
439
  @type args: list
440
  @param args: should either be an empty list, in which case
441
      we list data for all nodes, or contain a list of nodes
442
      to display data only for those
443
  @rtype: int
444
  @return: the desired exit code
445

    
446
  """
447
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
448
  if opts.user_storage_type is None:
449
    opts.user_storage_type = constants.ST_LVM_PV
450

    
451
  try:
452
    storage_type = _USER_STORAGE_TYPE[opts.user_storage_type]
453
  except KeyError:
454
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
455

    
456
  default_fields = {
457
    constants.ST_FILE: [
458
      constants.SF_NAME,
459
      constants.SF_USED,
460
      constants.SF_FREE,
461
      ],
462
    constants.ST_LVM_PV: [
463
      constants.SF_NAME,
464
      constants.SF_SIZE,
465
      constants.SF_USED,
466
      constants.SF_FREE,
467
      ],
468
    constants.ST_LVM_VG: [
469
      constants.SF_NAME,
470
      constants.SF_SIZE,
471
      ],
472
  }
473

    
474
  if opts.output is None:
475
    selected_fields = ["node"]
476
    selected_fields.extend(default_fields[storage_type])
477
  else:
478
    selected_fields = opts.output.split(",")
479

    
480
  op = opcodes.OpQueryNodeStorage(nodes=args,
481
                                  storage_type=storage_type,
482
                                  output_fields=selected_fields)
483
  output = SubmitOpCode(op)
484

    
485
  if not opts.no_headers:
486
    headers = {
487
      "node": "Node",
488
      constants.SF_NAME: "Name",
489
      constants.SF_SIZE: "Size",
490
      constants.SF_USED: "Used",
491
      constants.SF_FREE: "Free",
492
      constants.SF_ALLOCATABLE: "Allocatable",
493
      }
494
  else:
495
    headers = None
496

    
497
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
498
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
499

    
500
  data = GenerateTable(separator=opts.separator, headers=headers,
501
                       fields=selected_fields, unitfields=unitfields,
502
                       numfields=numfields, data=output, units=opts.units)
503

    
504
  for line in data:
505
    ToStdout(line)
506

    
507
  return 0
508

    
509

    
510
def ModifyVolume(opts, args):
511
  """Modify storage volume on a node.
512

    
513
  @param opts: the command line options selected by the user
514
  @type args: list
515
  @param args: should contain 3 items: node name, storage type and volume name
516
  @rtype: int
517
  @return: the desired exit code
518

    
519
  """
520
  (node_name, user_storage_type, volume_name) = args
521

    
522
  try:
523
    storage_type = _USER_STORAGE_TYPE[user_storage_type]
524
  except KeyError:
525
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
526

    
527
  changes = {}
528

    
529
  if opts.allocatable is not None:
530
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
531

    
532
  if changes:
533
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
534
                                     storage_type=storage_type,
535
                                     name=volume_name,
536
                                     changes=changes)
537
    SubmitOpCode(op)
538

    
539

    
540
def SetNodeParams(opts, args):
541
  """Modifies a node.
542

    
543
  @param opts: the command line options selected by the user
544
  @type args: list
545
  @param args: should contain only one element, the node name
546
  @rtype: int
547
  @return: the desired exit code
548

    
549
  """
550
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
551
    ToStderr("Please give at least one of the parameters.")
552
    return 1
553

    
554
  if opts.master_candidate is not None:
555
    candidate = opts.master_candidate == 'yes'
556
  else:
557
    candidate = None
558
  if opts.offline is not None:
559
    offline = opts.offline == 'yes'
560
  else:
561
    offline = None
562

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

    
573
  # even if here we process the result, we allow submit only
574
  result = SubmitOrSend(op, opts)
575

    
576
  if result:
577
    ToStdout("Modified node %s", args[0])
578
    for param, data in result:
579
      ToStdout(" - %-5s -> %s", param, data)
580
  return 0
581

    
582

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

    
695

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