gnt-node 19.3 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1 2 3
#!/usr/bin/python
#

4
# Copyright (C) 2006, 2007, 2008 Google Inc.
Iustin Pop's avatar
Iustin Pop committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


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

Iustin Pop's avatar
Iustin Pop committed
26 27 28 29
import sys
from optparse import make_option

from ganeti.cli import *
30
from ganeti import cli
Iustin Pop's avatar
Iustin Pop committed
31 32
from ganeti import opcodes
from ganeti import utils
33
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
34
from ganeti import errors
35
from ganeti import bootstrap
Iustin Pop's avatar
Iustin Pop committed
36 37


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

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

Michael Hanselmann's avatar
Michael Hanselmann committed
62

63
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
64
def AddNode(opts, args):
65 66 67 68 69 70 71
  """Add a node to the cluster.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the new node name
  @rtype: int
  @return: the desired exit code
72 73

  """
Iustin Pop's avatar
Iustin Pop committed
74
  cl = GetClient()
75 76
  dns_data = utils.HostInfo(args[0])
  node = dns_data.name
Iustin Pop's avatar
Iustin Pop committed
77 78 79 80
  readd = opts.readd

  try:
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
81
                           use_locking=False)
Iustin Pop's avatar
Iustin Pop committed
82 83 84 85 86 87 88 89 90 91 92 93
    node_exists, sip = output[0]
  except (errors.OpPrereqError, errors.OpExecError):
    node_exists = ""
    sip = None

  if readd:
    if not node_exists:
      ToStderr("Node %s not in the cluster"
               " - please retry without '--readd'", node)
      return 1
  else:
    if node_exists:
94
      ToStderr("Node %s already in the cluster (as %s)"
Iustin Pop's avatar
Iustin Pop committed
95
               " - please retry with '--readd'", node, node_exists)
96
      return 1
Iustin Pop's avatar
Iustin Pop committed
97
    sip = opts.secondary_ip
98

Iustin Pop's avatar
Iustin Pop committed
99 100 101 102
  # read the cluster name from the master
  output = cl.QueryConfigValues(['cluster_name'])
  cluster_name = output[0]

Iustin Pop's avatar
Iustin Pop committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  if readd:
    # clear the offline and drain flags on the node
    ToStdout("Resetting the 'offline' and 'drained' flags due to re-add")
    op = opcodes.OpSetNodeParams(node_name=node, force=True,
                                 offline=False, drained=False)

    result = SubmitOpCode(op, cl=cl)
    if result:
      ToStdout("Modified:")
      for param, data in result:
        ToStdout(" - %-5s -> %s", param, data)
  else:
    ToStderr("-- WARNING -- \n"
             "Performing this operation is going to replace the ssh daemon"
             " keypair\n"
             "on the target machine (%s) with the ones of the"
             " current one\n"
             "and grant full intra-cluster ssh root access to/from it\n", node)
121

Iustin Pop's avatar
Iustin Pop committed
122
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
123

Iustin Pop's avatar
Iustin Pop committed
124
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
125
                         readd=opts.readd)
Iustin Pop's avatar
Iustin Pop committed
126 127 128 129 130 131
  SubmitOpCode(op)


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

132 133 134 135 136 137
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
138 139
  """
  if opts.output is None:
140 141 142
    selected_fields = _LIST_DEF_FIELDS
  elif opts.output.startswith("+"):
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
Iustin Pop's avatar
Iustin Pop committed
143 144 145
  else:
    selected_fields = opts.output.split(",")

Iustin Pop's avatar
Iustin Pop committed
146
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
Iustin Pop's avatar
Iustin Pop committed
147 148

  if not opts.no_headers:
149
    headers = _LIST_HEADERS
150 151 152
  else:
    headers = None

153
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
154

155 156
  numfields = ["dtotal", "dfree",
               "mtotal", "mnode", "mfree",
157
               "pinst_cnt", "sinst_cnt",
158
               "ctotal", "serial_no"]
159

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

176 177
  data = GenerateTable(separator=opts.separator, headers=headers,
                       fields=selected_fields, unitfields=unitfields,
178
                       numfields=numfields, data=output, units=opts.units)
179
  for line in data:
180
    ToStdout(line)
Iustin Pop's avatar
Iustin Pop committed
181 182 183 184

  return 0


Iustin Pop's avatar
Iustin Pop committed
185 186 187
def EvacuateNode(opts, args):
  """Relocate all secondary instance from a node.

188 189 190 191 192 193
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
194
  """
195
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
196
  force = opts.force
197 198 199 200 201 202 203 204 205

  dst_node = opts.dst_node
  iallocator = opts.iallocator

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

Iustin Pop's avatar
Iustin Pop committed
206
  selected_fields = ["name", "sinst_list"]
207
  src_node = args[0]
Iustin Pop's avatar
Iustin Pop committed
208

209
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
210
                         use_locking=False)
Iustin Pop's avatar
Iustin Pop committed
211 212 213
  src_node, sinst = result[0]

  if not sinst:
214
    ToStderr("No secondary instances on node %s, exiting.", src_node)
Iustin Pop's avatar
Iustin Pop committed
215 216
    return constants.EXIT_SUCCESS

217
  if dst_node is not None:
218 219
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
                           use_locking=False)
220 221 222 223 224 225 226 227 228 229
    dst_node = result[0][0]

    if src_node == dst_node:
      raise errors.OpPrereqError("Evacuate node needs different source and"
                                 " target nodes (node %s given twice)" %
                                 src_node)
    txt_msg = "to node %s" % dst_node
  else:
    txt_msg = "using iallocator %s" % iallocator

Iustin Pop's avatar
Iustin Pop committed
230 231 232
  sinst = utils.NiceSort(sinst)

  if not force and not AskUser("Relocate instance(s) %s from node\n"
233
                               " %s %s?" %
Iustin Pop's avatar
Iustin Pop committed
234
                               (",".join("'%s'" % name for name in sinst),
235
                               src_node, txt_msg)):
Iustin Pop's avatar
Iustin Pop committed
236 237
    return constants.EXIT_CONFIRMATION

238
  ops = []
Iustin Pop's avatar
Iustin Pop committed
239 240
  for iname in sinst:
    op = opcodes.OpReplaceDisks(instance_name=iname,
241
                                remote_node=dst_node,
242
                                mode=constants.REPLACE_DISK_CHG,
243
                                iallocator=iallocator,
244
                                disks=[])
245
    ops.append(op)
246

247 248
  job_id = cli.SendJob(ops, cl=cl)
  cli.PollJob(job_id, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
249 250


Iustin Pop's avatar
Iustin Pop committed
251 252 253
def FailoverNode(opts, args):
  """Failover all primary instance on a node.

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

Iustin Pop's avatar
Iustin Pop committed
260
  """
261
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
262 263 264
  force = opts.force
  selected_fields = ["name", "pinst_list"]

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

  if not pinst:
272
    ToStderr("No primary instances on node %s, exiting.", node)
Iustin Pop's avatar
Iustin Pop committed
273 274 275 276 277 278 279 280 281 282
    return 0

  pinst = utils.NiceSort(pinst)

  retcode = 0

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

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


Iustin Pop's avatar
Iustin Pop committed
298 299 300 301 302 303 304 305
def MigrateNode(opts, args):
  """Migrate all primary instance on a node.

  """
  cl = GetClient()
  force = opts.force
  selected_fields = ["name", "pinst_list"]

306
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
Iustin Pop's avatar
Iustin Pop committed
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
  node, pinst = result[0]

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

  pinst = utils.NiceSort(pinst)

  retcode = 0

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

  jex = JobExecutor(cl=cl)
  for iname in pinst:
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
                                   cleanup=False)
    jex.QueueJob(iname, op)

  results = jex.GetResults()
  bad_cnt = len([row for row in results if not row[0]])
  if bad_cnt == 0:
    ToStdout("All %d instance(s) migrated successfully.", len(results))
  else:
    ToStdout("There were errors during the migration:\n"
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
  return retcode


Iustin Pop's avatar
Iustin Pop committed
337 338 339
def ShowNodeConfig(opts, args):
  """Show node information.

340 341 342 343 344 345 346 347
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should either be an empty list, in which case
      we show information about all nodes, or should contain
      a list of nodes to be queried for information
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
348
  """
349 350
  cl = GetClient()
  result = cl.QueryNodes(fields=["name", "pip", "sip",
351 352
                                 "pinst_list", "sinst_list",
                                 "master_candidate", "drained", "offline"],
353
                         names=args, use_locking=False)
Iustin Pop's avatar
Iustin Pop committed
354

355 356
  for (name, primary_ip, secondary_ip, pinst, sinst,
       is_mc, drained, offline) in result:
357 358 359
    ToStdout("Node name: %s", name)
    ToStdout("  primary ip: %s", primary_ip)
    ToStdout("  secondary ip: %s", secondary_ip)
360 361 362
    ToStdout("  master candidate: %s", is_mc)
    ToStdout("  drained: %s", drained)
    ToStdout("  offline: %s", offline)
Iustin Pop's avatar
Iustin Pop committed
363
    if pinst:
364
      ToStdout("  primary for instances:")
365
      for iname in utils.NiceSort(pinst):
366
        ToStdout("    - %s", iname)
Iustin Pop's avatar
Iustin Pop committed
367
    else:
368
      ToStdout("  primary for no instances")
Iustin Pop's avatar
Iustin Pop committed
369
    if sinst:
370
      ToStdout("  secondary for instances:")
371
      for iname in utils.NiceSort(sinst):
372
        ToStdout("    - %s", iname)
Iustin Pop's avatar
Iustin Pop committed
373
    else:
374
      ToStdout("  secondary for no instances")
Iustin Pop's avatar
Iustin Pop committed
375 376 377 378 379

  return 0


def RemoveNode(opts, args):
380 381 382 383 384 385 386 387 388 389
  """Remove a node from the cluster.

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

  """
Iustin Pop's avatar
Iustin Pop committed
390 391
  op = opcodes.OpRemoveNode(node_name=args[0])
  SubmitOpCode(op)
392
  return 0
Iustin Pop's avatar
Iustin Pop committed
393 394


Iustin Pop's avatar
Iustin Pop committed
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
def PowercycleNode(opts, args):
  """Remove a node from the cluster.

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

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

  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
  result = SubmitOpCode(op)
  ToStderr(result)
  return 0


417 418 419
def ListVolumes(opts, args):
  """List logical volumes on node(s).

420 421 422 423 424 425 426 427
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should either be an empty list, in which case
      we list data for all nodes, or contain a list of nodes
      to display data only for those
  @rtype: int
  @return: the desired exit code

428 429 430 431 432 433 434 435 436 437 438
  """
  if opts.output is None:
    selected_fields = ["node", "phys", "vg",
                       "name", "size", "instance"]
  else:
    selected_fields = opts.output.split(",")

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

  if not opts.no_headers:
439 440 441 442 443 444
    headers = {"node": "Node", "phys": "PhysDev",
               "vg": "VG", "name": "Name",
               "size": "Size", "instance": "Instance"}
  else:
    headers = None

445
  unitfields = ["size"]
446 447 448

  numfields = ["size"]

449 450
  data = GenerateTable(separator=opts.separator, headers=headers,
                       fields=selected_fields, unitfields=unitfields,
451
                       numfields=numfields, data=output, units=opts.units)
452 453

  for line in data:
454
    ToStdout(line)
455 456 457 458

  return 0


Iustin Pop's avatar
Iustin Pop committed
459 460 461 462 463 464 465 466 467 468
def SetNodeParams(opts, args):
  """Modifies a node.

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

  """
469
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
Iustin Pop's avatar
Iustin Pop committed
470 471 472
    ToStderr("Please give at least one of the parameters.")
    return 1

473 474 475 476 477 478 479 480
  if opts.master_candidate is not None:
    candidate = opts.master_candidate == 'yes'
  else:
    candidate = None
  if opts.offline is not None:
    offline = opts.offline == 'yes'
  else:
    offline = None
481 482 483 484 485

  if opts.drained is not None:
    drained = opts.drained == 'yes'
  else:
    drained = None
Iustin Pop's avatar
Iustin Pop committed
486
  op = opcodes.OpSetNodeParams(node_name=args[0],
487 488
                               master_candidate=candidate,
                               offline=offline,
489
                               drained=drained,
490
                               force=opts.force)
Iustin Pop's avatar
Iustin Pop committed
491 492 493 494 495 496 497 498 499 500 501

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

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


Iustin Pop's avatar
Iustin Pop committed
502 503 504 505 506
commands = {
  'add': (AddNode, ARGS_ONE,
          [DEBUG_OPT,
           make_option("-s", "--secondary-ip", dest="secondary_ip",
                       help="Specify the secondary ip for the node",
507 508 509 510
                       metavar="ADDRESS", default=None),
           make_option("--readd", dest="readd",
                       default=False, action="store_true",
                       help="Readd old node after replacing it"),
511 512 513
           make_option("--no-ssh-key-check", dest="ssh_key_check",
                       default=True, action="store_false",
                       help="Disable SSH key fingerprint checking"),
514
           ],
515 516
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
          "Add a node to the cluster"),
517 518 519 520 521
  'evacuate': (EvacuateNode, ARGS_ONE,
               [DEBUG_OPT, FORCE_OPT,
                make_option("-n", "--new-secondary", dest="dst_node",
                            help="New secondary node", metavar="NODE",
                            default=None),
522
                make_option("-I", "--iallocator", metavar="<NAME>",
523 524 525 526 527
                            help="Select new secondary for the instance"
                            " automatically using the"
                            " <NAME> iallocator plugin",
                            default=None, type="string"),
                ],
528
               "[-f] {-I <iallocator> | -n <dst>} <node>",
529 530
               "Relocate the secondary instances from a node"
               " to other nodes (only for instances with drbd disk template)"),
Iustin Pop's avatar
Iustin Pop committed
531 532 533 534 535 536 537
  'failover': (FailoverNode, ARGS_ONE,
               [DEBUG_OPT, FORCE_OPT,
                make_option("--ignore-consistency", dest="ignore_consistency",
                            action="store_true", default=False,
                            help="Ignore the consistency of the disks on"
                            " the secondary"),
                ],
538
               "[-f] <node>",
Iustin Pop's avatar
Iustin Pop committed
539
               "Stops the primary instances on a node and start them on their"
540
               " secondary node (only for instances with drbd disk template)"),
Iustin Pop's avatar
Iustin Pop committed
541 542 543 544 545 546 547 548 549 550 551 552
  'migrate': (MigrateNode, ARGS_ONE,
               [DEBUG_OPT, FORCE_OPT,
                make_option("--non-live", dest="live",
                            default=True, action="store_false",
                            help="Do a non-live migration (this usually means"
                            " freeze the instance, save the state,"
                            " transfer and only then resume running on the"
                            " secondary node)"),
                ],
               "[-f] <node>",
               "Migrate all the primary instance on a node away from it"
               " (only for instances of type drbd)"),
Iustin Pop's avatar
Iustin Pop committed
553
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
554
           "[<node_name>...]", "Show information about the node(s)"),
Iustin Pop's avatar
Iustin Pop committed
555 556 557 558
  'list': (ListNodes, ARGS_ANY,
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
           "[nodes...]",
           "Lists the nodes in the cluster. The available fields"
559 560 561
           " are (see the man page for details): %s"
           " The default field list is (in order): %s." %
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
Iustin Pop's avatar
Iustin Pop committed
562 563 564 565 566
  'modify': (SetNodeParams, ARGS_ONE,
             [DEBUG_OPT, FORCE_OPT,
              SUBMIT_OPT,
              make_option("-C", "--master-candidate", dest="master_candidate",
                          choices=('yes', 'no'), default=None,
567
                          metavar="yes|no",
Iustin Pop's avatar
Iustin Pop committed
568
                          help="Set the master_candidate flag on the node"),
569 570

              make_option("-O", "--offline", dest="offline", metavar="yes|no",
571 572
                          choices=('yes', 'no'), default=None,
                          help="Set the offline flag on the node"),
573
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
574 575
                          choices=('yes', 'no'), default=None,
                          help="Set the drained flag on the node"),
Iustin Pop's avatar
Iustin Pop committed
576 577
              ],
             "<instance>", "Alters the parameters of an instance"),
Iustin Pop's avatar
Iustin Pop committed
578 579
  'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
                 "<node_name>", "Tries to forcefully powercycle a node"),
Iustin Pop's avatar
Iustin Pop committed
580
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
581
             "<node_name>", "Removes a node from the cluster"),
582 583
  'volumes': (ListVolumes, ARGS_ANY,
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
584
              "[<node_name>...]", "List logical volumes on node(s)"),
585
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
586
                "<node_name>", "List the tags of the given node"),
587
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
588
               "<node_name> tag...", "Add tags to the given node"),
589
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
590
                  "<node_name> tag...", "Remove tags from the given node"),
Iustin Pop's avatar
Iustin Pop committed
591 592 593 594
  }


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